[
  {
    "path": ".gitignore",
    "content": ".vscode/\n__pycache__/\n.DS_Store\n*.pyc\n*.zip\nlogs/\nenvs/webshop/data/\nenvs/webshop/search_index/\nenvs/webshop/data.zip\nenvs/webshop/indexes.zip\nprofiles.yaml"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2026 Foundation Agents\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ReCode: Unify Plan and Action for Universal Granularity Control\n\n[![Arxiv](https://img.shields.io/badge/2510.23564-arXiv-red)](https://arxiv.org/abs/2510.23564)\n\n> If you encounter any difficulties in using or reproducing the code, please contact me at [zhaoyangyu713@gmail.com](mailto:zhaoyangyu713@gmail.com).\n\nReCode introduces recursive code generation for LLM agents, unifying plan and action into a single representation. By treating high-level plans as placeholder functions that recursively decompose into executable primitives, it achieves universal granularity control and dynamically adapts from strategic thinking to concrete actions. This repository hosts the reference implementation used in the paper, along with environment wrappers and experiment tooling.\n\n<p align=\"center\">\n<a href=\"\"><img src=\"figures/figure1-comparison.jpg\" alt=\"A comparison of LLM-based agent decision-making paradigms\" title=\"A comparison of LLM-based agent decision-making paradigms\" width=\"92%\"></a>\n</p>\n\n## Core Idea\n\nReCode adopts a divide-and-conquer strategy, decomposing complex tasks into executable code fragments:\n\n1. **Tree-structured code**: Organizes partial programs in a tree where each node captures one sub-task and records its execution trace.\n2. **Recursive expansion**: Placeholder functions are expanded by the LLM into more specific calls or smaller subroutines using environment-specific prompts and few-shots.\n3. **Dynamic execution loop**: Each node is executed immediately; fresh observations decide whether to expand further, retry, or finish.\n4. **Shared executor state**: A constrained Python executor maintains environment variables, validates code blocks, and exposes the toolset available to the agent.\n\n<p align=\"center\">\n<a href=\"\"><img src=\"figures/figure2-method-new.jpg\" alt=\"An overview of ReCode\" title=\"An overview of ReCode\" width=\"92%\"></a>\n</p>\n\n## Repository Layout\n\n- `run.py` – CLI entry point that instantiates agents/envs, manages concurrency, and writes run summaries.\n- `agents/recode/` – ReCode agent implementation, prompt templates, and utility helpers.\n- `envs/` – Environment wrappers and assets for `alfworld`, `webshop`, and `sciworld`.\n- `configs/` – LLM profile templates and (expected) pricing metadata used by the async client.\n- `utils/` – Shared components: async OpenAI wrapper, constrained executor, logging helpers, error types.\n- `figures/` – Paper figures used throughout this README.\n\n## Experiments\n\nTo evaluate the effectiveness of ReCode, we divide our experiments into the inference part and the training part.\n\n1. **Inference Result**: we compare against several mainstream paradigm (ReAct, CodeAct) and some of the work focused on improving LLM-based agent planning (AdaPlanner and ADaPT). ReCode achieved significant performance improvements across all three environments, with an average score of 60.8, surpassing the best baseline method by 10.5 (relative 20.9%). _With our tests, ReCode can achieve a perfect **100** score in ALFWorld under `claude-4-sonnet`._\n\n\n\n<p align=\"center\">\n<a href=\"\"><img src=\"figures/inference-result.png\" alt=\"Inference performance across environments\" title=\"Inference evaluation summary\" width=\"92%\"></a>\n</p>\n\n2. **Training Result**: we conduct supervised fine-tuning (SFT) on ReCode, ReAct and CodeAct with `Qwen2.5-7B-Instruct`. ReCode+SFT delivers an impressive average performance of 70.4% across all environments, outperforming both ReAct+SFT (67.6%) and CodeAct+SFT (55.8%), highlighting its exceptional data efficiency.\n\n<p align=\"center\">\n<a href=\"\"><img src=\"figures/sft-data.png\" alt=\"SFT performance across environments\" title=\"SFT evaluation summary\" width=\"92%\"></a>\n</p>\n\n<p align=\"center\">\n<a href=\"\"><img src=\"figures/sft-result.png\" alt=\"SFT performance across environments\" title=\"SFT evaluation summary\" width=\"92%\"></a>\n</p>\n\n## Quick Start\n\nTo run ReCode, we need a conda environment. The python version should be 3.10 or newer.\n\nThen, it is necessary to configure dependencies for three environments (it has not been confirmed whether conflicts will arise in the same environment), and we suggest configuring them in three separate environments.\n\n```bash\nconda create -n recode-envname python=3.10 # Replace \"envname\" with the your environment name.\nconda activate recode-envname\n```\n\n---\n\n### ALFWorld\n- Follow the [ALFWorld instructions](https://github.com/alfworld/alfworld).\n- Set `ALFWORLD_DATA` to the dataset root or edit `envs/alfworld/base_config.yaml` to point to your local paths:\n\n  ```bash\n  export ALFWORLD_DATA=/path/to/alfworld\n  ```\n\n### ScienceWorld\n- Follow the instruciton from the [ScienceWorld repository](https://github.com/allenai/ScienceWorld).\n\n### WebShop\nThanks to [ETO ](https://github.com/Yifan-Song793/ETO) for providing a convenient script to configure WebShop environment.\n\n```bash\ncd envs/webshop\npip install -e .\nconda install -y -c conda-forge openjdk=11\npip install \"en_core_web_lg @ https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.6.0/en_core_web_lg-3.6.0-py3-none-any.whl\"\n```\n\nRun the provided helper to fetch the goal set and pre-built search index:\n\n```bash\n# The current path is \"envs/webshop\"\nbash setup.sh\n```\n\n---\n\nInstall some other dependencies.\n\n```bash\npip install -r requirements.txt # Here may not be complete, please contact me promptly if you encounter any problems\n```\n\nEnsure `configs/profiles.yaml` points to a valid API credential (copy `configs/profiles_example.yaml` if you need a template), then run a short dry run in any enabled environment:\n```bash\npython run.py -a recode -e alfworld -n 1 --split test --profile default\n```\nReplace `alfworld` with `webshop` or `sciworld` once their assets are available. Logs are written to `logs/<run_id>/`, and the console prints a condensed summary for quick diagnostics.\n\n## Configure LLM Access\n\n- `configs/profiles.yaml` contains named profiles. The `run.py --profile` flag selects which profile to forward to `AsyncLLM`. Example:\n\n  ```yaml\n  models:\n    default:\n      api_key: \"sk-your_api_key\"\n      base_url: \"https://api.openai.com/v1\"\n      model: \"gpt-4o-mini\"\n      temperature: 0.0\n      track_costs: true\n    gpt-4o:\n      api_key: \"sk-your_other_key\"\n      base_url: \"https://api.openai.com/v1\"\n      model: \"gpt-4o\"\n      temperature: 0.7\n      max_tokens: 512\n  ```\n\n- Cost tracking loads `configs/prices.json`. If you do not want to record costs, set `track_costs: false` for the profile.\n- As a fallback, you can omit the file and set `OPENAI_API_KEY` in the environment; the default profile will then use it.\n- A ready-to-edit template lives at `configs/profiles_example.yaml`; copy it to `configs/profiles.yaml` if you're starting from scratch:\n  ```bash\n  cp configs/profiles_example.yaml configs/profiles.yaml\n  ```\n\n## Running ReCode\n\n`run.py` is the canonical entry point. It resolves agent/environment aliases, manages concurrency, streams logs, and emits a structured summary.\n\n```bash\n# ALFWorld, single instance\npython run.py -a recode -e alfworld -n 1 --split test --profile default\n\n# WebShop, 3 test goals, allow deeper recursion\npython run.py -a recode -e webshop -n 3 --split test --profile default --max-depth 12\n\n# ScienceWorld, run 5 instances with 2-way concurrency\npython run.py -a recode -e sciworld -n 5 -c 2 --profile gpt-4o\n```\n\nKey CLI flags:\n- `-a / --agent` – class path or alias (`recode` resolves to `agents.recode.agent.ReCodeAgent`).\n- `-e / --env` – environment class or alias (`alfworld`, `webshop`, `sciworld`).\n- `-n / --instances` – number of evaluation episodes.\n- `-c / --concurrent` – max concurrent episodes (rich progress UI automatically adapts).\n- `--split`, `--seed`, `--max-depth`, `--profile` – forwarded to both agent and environment.\n- `-C / --config` – YAML file whose keys override CLI flags; useful for complex sweeps.\n\nExample YAML (`configs/example.yaml`):\n\n```yaml\nagent: recode\nenv: alfworld\ninstances: 10\nconcurrent: 2\nprofile: gpt-4o\nsplit: test\ntask_types: [\"put\", \"clean\"] # For ALFWorld\nmax_depth: 12\nmax_retry: 4\n```\n\nRun it with:\n\n```bash\npython run.py -C configs/example.yaml\n```\n\n## Logging & Results\n\n- Each run creates `logs/<run_id>/` with:\n  - `running_logs/run.log` – aggregated stream of agent + environment logs.\n  - `running_logs/instance_<id>.log` – per-instance traces (when multiple instances are launched).\n  - `<results.json>` – structured summary written by `write_summary`, containing per-instance metrics and aggregated statistics (overall + per task type).\n- The console prints a condensed summary (success rate, standard metrics, by-task breakdown) after completion.\n\n## Extending to New Environments\n\n1. **Implement the `Env` interface** under `envs/<your_env>/env.py`. Use `base.environment.Env` as the contract: implement `reset`, `_run`, `is_done`, `is_success`, and `report`. Return `{\"observations\": [...], \"env_name\": <name>, \"env\": self}` from `reset`.\n2. **Expose prompts and guidance** in `agents/recode/resources/`:\n   - `prompts/<env_name>/actions.txt` – concise description of valid `run(\"...\")` calls/tools.\n   - `fewshots/<env_name>/` – one or more `.txt` examples showing thought→execute patterns.\n   - If your environment has task types, update `agents/recode/agent.py::_load_resources` and `agents/recode/utils.parse_raw_observation` to parse initial observations correctly.\n3. **Register aliases** by adding your class to `ENV_ALIASES` in `run.py` (optional but convenient) and, if needed, plan-specific logic in the agent utilities.\n4. Optionally add setup scripts (similar to `envs/webshop/setup.sh`) to document dataset fetching.\n\n## Programmatic Use\n\nYou can embed the agent directly inside your own loop by reusing the provided utilities:\n\n```python\nimport asyncio\n\nfrom agents.recode.agent import ReCodeAgent\nfrom envs.alfworld.env import AlfworldEnv\n\nasync def solve_once():\n    config = {\"split\": \"test\", \"task_types\": [\"put\"], \"max_depth\": 10}\n    env = AlfworldEnv(logger=None)\n    agent = ReCodeAgent()\n    init_info = env.reset(config)\n    agent.reset(config, init_info)\n\n    observations = init_info[\"observations\"]\n    while not env.is_done():\n        actions = await agent.act(observations)\n        observations = await env.run(actions)\n\n    print(env.report())\n    await env.close()\n\nasyncio.run(solve_once())\n```\n\nThe same pattern works for any `Env` implementation; be sure to pass a logger if you need file-backed traces.\n\n## Citation\n\n```\n@misc{yu2025recodeunifyplanaction,\n      title={ReCode: Unify Plan and Action for Universal Granularity Control}, \n      author={Zhaoyang Yu and Jiayi Zhang and Huixue Su and Yufan Zhao and Yifan Wu and Mingyi Deng and Jinyu Xiang and Yizhang Lin and Lingxiao Tang and Yingchao Li and Yuyu Luo and Bang Liu and Chenglin Wu},\n      year={2025},\n      eprint={2510.23564},\n      archivePrefix={arXiv},\n      primaryClass={cs.AI},\n      url={https://arxiv.org/abs/2510.23564}, \n}\n```\n"
  },
  {
    "path": "agents/recode/agent.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom enum import Enum\nfrom typing import List, Optional\nfrom datetime import datetime, timezone\n\nfrom base.agent import Agent\nfrom utils.llm import AsyncLLM\nfrom utils.executor import Executor\nfrom utils.common import parse_xml_tag\n\nfrom agents.recode.resources.prompts.default_new import EXPAND_PROMPT\nfrom agents.recode.utils import (\n    parse_raw_observation,\n    split_blocks,\n    validate_blocks,\n    NodeStatus,\n    CodeNode,\n    get_variables,\n)\n\nDEFAULT_MAX_DEPTH = 10\nDEFAULT_MAX_RETRY = 5\nDEFAULT_MAX_REWRITE = 5\n\n\nclass ReCodeAgent(Agent):\n    def __init__(\n        self,\n        logger=None,\n        task_type: str = None,\n    ) -> None:\n        self.logger = logger\n        self.llm = AsyncLLM()\n        self.executor = Executor(if_run_print=True)\n\n        self.root: Optional[CodeNode] = None\n        self.current_node: Optional[CodeNode] = None\n        self.previous_node: Optional[CodeNode] = None\n        self.task_type: str = task_type\n        self.is_start = False\n \n    def reset(self, running_config: dict, init_info: dict=None) -> None:\n        self.root = None\n        self.current_node = None\n        self.previous_node = None\n        self.is_start = False\n\n        self.max_depth: int = running_config.get('max_depth') or DEFAULT_MAX_DEPTH\n        self.max_retry: int = running_config.get('max_retry') or DEFAULT_MAX_RETRY\n        self.max_rewrite: int = running_config.get('max_rewrite') or DEFAULT_MAX_REWRITE\n        \n        if init_info and 'task_type' in init_info and init_info['task_type']:\n            self.task_type = init_info['task_type'].lower()\n        elif 'task_type' in running_config:\n            self.task_type = running_config['task_type'].lower()\n\n        if \"profile\" in running_config and running_config['profile']:\n            self.logger.info(f\"Using profile: {running_config['profile']}\")\n            self.llm = AsyncLLM(running_config['profile'])\n\n        assert 'env_name' in init_info, \"Envrioment must be specified\"\n        self.env_name = init_info['env_name']\n        if self.env_name == \"alfworld\":\n            self.logger.info(\"Setting max steps to 80\")\n            init_info['env'].set_max_steps(80)\n        self.executor.set_env(init_info['env'])\n\n        self._load_resources()\n\n    def _load_resources(self):\n        resources_path = Path(\"agents/recode/resources/prompts\") / self.env_name\n        self.available_actions = open(resources_path / \"actions.txt\", \"r\").read()\n\n        fewshots_path = Path(\"agents/recode/resources/fewshots\") / self.env_name\n        if self.env_name == \"alfworld\":\n            self.fewshots = open(fewshots_path / f\"{self.task_type}.txt\", \"r\").read()\n        elif self.env_name == \"webshop\":\n            self.fewshots = open(fewshots_path / \"base.txt\", \"r\").read()\n            # self.fewshots = \"(No Examples)\"\n        elif self.env_name == \"sciworld\":\n            self.fewshots = open(fewshots_path / \"base.txt\", \"r\").read()\n        else:\n            raise ValueError(f\"Unsupported environment in _load_resources: {self.env_name}\")\n\n    async def act(self, observations: List[str]) -> List[str]:\n        if not self.is_start:\n            assert len(observations) == 1, \"Only one observation is allowed for the first node\"\n            self._init_code_tree(observations[0])\n            self.is_start = True\n\n        if self.current_node.status == NodeStatus.STUB:\n            await self._handle_stub()\n        elif self.current_node.status == NodeStatus.ERROR:\n            return [\"[FINISH]\"]\n\n        if not self.current_node:\n            return [\"[FINISH]\"]\n        \n        self.logger.info(f\"[Execute]\\n{self.current_node.code}\")\n        result = self._execute(self.current_node.code)\n        self.current_node.observations.extend(result[\"stdout\"]) if result[\"stdout\"] else None\n        self.logger.info(f\"[Exec Result]\\n{result}\")\n\n        if result[\"success\"]:\n            self.logger.info(f\"[Execution Stdout] {result['stdout']}\")\n            self.current_node.status = NodeStatus.COMPLETED\n            self.previous_node = self.current_node\n            self.current_node = self.current_node.next()\n            if not self.current_node:\n                return [\"[FINISH]\"]\n        else:\n            if \"NeedExpansion\" in result[\"error\"]:\n                self.current_node.status = NodeStatus.STUB\n            else:\n                self.current_node.status = NodeStatus.ERROR\n                self.current_node.error = result[\"error\"]\n\n    async def _handle_stub(self) -> None:\n        if self.current_node and self.current_node.depth >= self.max_depth:\n            if self.logger:\n                self.logger.warning(\"Max depth reached - terminating.\")\n            self.current_node = None\n            return\n\n        new_blocks = await self._expand()\n        self.logger.info(\"[NEW_BLOCKS]\\n\" + \"\\n\".join(new_blocks)) if new_blocks else None\n\n        if self.current_node:\n            if new_blocks is None:\n                self.current_node = None\n                return\n            if new_blocks:\n                for block in new_blocks:\n                    child_node = CodeNode(code=block, parent=self.current_node)\n                    self.current_node.children.append(child_node)\n            else: \n                self.current_node.status = NodeStatus.SKIP\n\n        self.current_node = self.current_node.next()\n\n    async def _expand(self) -> Optional[List[str]]:\n        attempt = 0\n        retry_hint_added = False\n        while True:\n            user_prompt = self._build_expand_prompt()\n            if retry_hint_added:\n                user_prompt += (\n                    \"\\n\\n[Important] Your previous expansion produced syntactically invalid code and/or included disallowed constructs (e.g., def/async def). \"\n                    \"Strictly follow the rules: output a single valid Python code block, and do not use def or async def.\"\n                )\n            if self.logger:\n                self.logger.info(\"[LLM_IN]\\n\" + user_prompt)\n            response, _cost = await self.llm(user_prompt)\n            if self.logger:\n                self.logger.info(\"[LLM_OUT]\\n\" + response.strip())\n\n            thought = parse_xml_tag(response, \"think\").strip()\n            self.current_node.thought = thought\n            expanded_code = parse_xml_tag(response, \"execute\").strip()\n\n            try:\n                blocks = split_blocks(expanded_code)\n                validate_blocks(blocks)\n                return blocks\n            except (SyntaxError, ValueError) as e:\n                attempt += 1\n                retry_hint_added = True\n                if attempt >= self.max_rewrite:\n                    if self.logger:\n                        self.logger.info(\n                            f\"[STOP] Reached max re-expands ({self.max_rewrite}). Last error: {e}. Ending episode.\"\n                        )\n                    return None\n                if self.logger:\n                    self.logger.info(\n                        f\"[RE-EXPAND {attempt}/{self.max_rewrite}] Split/validation failed due to: {e}. Re-asking EXPAND...\"\n                    )\n\n    def _execute(self, code: str) -> dict:\n        return self.executor.execute(code)\n\n    def _init_code_tree(self, observation: str) -> None:\n        self.logger.info(f\"[OBSERVATIONS]\\n{observation}\")\n        initial_observation, instruction = parse_raw_observation(observation, self.env_name)\n        self.executor.set_var('observation', initial_observation)\n        self.executor.set_var('instruction', instruction)\n        self.root = CodeNode(code=f\"solve(instruction, observation)\")\n        self.current_node = self.root\n        \n    def _build_expand_prompt(self) -> str:\n        # available_actions, examples, task, variables\n        examples = self.fewshots if self.fewshots else \"(No Examples)\"\n        task = self.current_node.code\n        variables = get_variables(self.executor, self.current_node.code)\n        variables = variables if variables else \"(No Variables)\"\n        return EXPAND_PROMPT.format(available_actions=self.available_actions, examples=examples, task=task, variables=variables)\n\n    def _get_max_depth(self, node: Optional[CodeNode]) -> int:\n        if node is None:\n            return 0\n        max_depth = node.depth\n        for child in node.children:\n            child_max = self._get_max_depth(child)\n            if child_max > max_depth:\n                max_depth = child_max\n        return max_depth\n\n    def _get_formatted_tree(self) -> dict:\n        version = \"recode.plan.v1\"\n\n        meta = {\n            \"env_name\": getattr(self, \"env_name\", None),\n            \"task_type\": getattr(self, \"task_type\", None),\n            \"created_at\": datetime.now(timezone.utc).isoformat(),\n            \"max_depth\": getattr(self, \"max_depth\", None),\n            \"max_retry\": getattr(self, \"max_retry\", None),\n            \"max_rewrite\": getattr(self, \"max_rewrite\", None),\n        }\n\n        nodes = {}\n        edges = []\n        root_id = self.root.id if self.root else None\n\n        if self.root:\n            stack = [self.root]\n            while stack:\n                node = stack.pop()\n                nodes[node.id] = {\n                    \"code\": node.code,\n                    \"thought\": getattr(node, \"thought\", None),\n                    \"status\": node.status.value if isinstance(node.status, Enum) else node.status,\n                    \"depth\": node.depth,\n                    \"observations\": list(node.observations) if node.observations else [],\n                    \"error\": node.error,\n                }\n                for child in node.children:\n                    edges.append([node.id, child.id])\n                # Preserve order by pushing children in reverse for DFS\n                for child in reversed(node.children):\n                    stack.append(child)\n\n        return {\n            \"version\": version,\n            \"meta\": meta,\n            \"root_id\": root_id,\n            \"nodes\": nodes,\n            \"edges\": edges,\n        }\n\n\n    def report(self) -> dict:\n        return {\n            'cost': self.llm.spent,\n            'tree': self._get_formatted_tree(),\n            'max_depth': self._get_max_depth(self.root)\n        }"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/clean.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: clean some apple and put it in sidetable.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 1, a diningtable 3, a diningtable 2, a diningtable 1, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a sidetable 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nI first characterize the task: the object is an apple that must be cleaned (so the cleaning station is the only sinkbasin present) and finally placed on the sidetable. The initial observation provides a single `sinkbasin 1` and `sidetable 1`, so I persist both IDs. I also parse and persist all location IDs for later search. The expansion should be decomposed into three concrete phases: (1) acquire an apple by searching openable and non-openable receptacles; handle closures by opening when needed; once found, take the apple by its exact ID; (2) clean it strictly at the sinkbasin; (3) place it on the target surface, ensuring it is reachable (surfaces are non-openable, so navigation suffices). Typical mistakes to avoid include assuming an object ID without extracting it from observations, forgetting to open a closed container before taking, or trying to clean anywhere other than the sinkbasin.\n</think>\n\n<execute>\ncleaning_location_ID = 'sinkbasin 1'\ntarget_location_ID = 'sidetable 1'\nobj = 'apple'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nclean_with(obj_ID, cleaning_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put a clean lettuce in diningtable.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 13, a cabinet 12, a cabinet 11, a cabinet 10, a cabinet 9, a cabinet 8, a cabinet 7, a cabinet 6, a cabinet 5, a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 1, a diningtable 1, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a shelf 3, a shelf 2, a shelf 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nThe required object is lettuce, and the final location is the dining table. The cleaning step is mandatory and must use the unique `sinkbasin 1`. I will persist `sinkbasin 1` as the cleaning site and `diningtable 1` as the goal. I will also build `all_location_IDs` from the initial observation. The decomposition mirrors the previous case: systematically search and take a lettuce, clean it at the sink, then navigate to the dining table and place the item. I must be careful to open any closed storage (e.g., cabinet, drawer, fridge, microwave) before taking and avoid using move to a closed container. Finally, I will ensure the regex extracts the exact lettuce ID from the observation.\n</think>\n\n<execute>\ncleaning_location_ID = 'sinkbasin 1'\ntarget_location_ID = 'diningtable 1'\nobj = 'lettuce'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nclean_with(obj_ID, cleaning_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nclean_with(obj_ID, cleaning_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- cleaning_location_ID (str): sinkbasin 1\n\n[Output]\n<think>\nCleaning must be performed at the sinkbasin only. I will explicitly navigate to the recorded cleaning site and issue the cleaning command with the exact object ID. I do not need to open the sink; I only need to be there. A typical error is to attempt to clean somewhere else or to forget to navigate first.\n</think>\n\n<execute>\nrun(f'go to {cleaning_location_ID}')\nrun(f'clean {obj_ID} with {cleaning_location_ID}')\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_in(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nI will navigate to the target location, examine if nothing happens, determine if it is closed, and open if needed. Then I will move the object in. This logic supports both containers (openable) and surfaces (non-openable, but harmless to check). Pitfalls: using the wrong ID in `examine` or skipping the `location_closed` check.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/cool.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put a cool mug in shelf.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 9, a cabinet 8, a cabinet 7, a cabinet 6, a cabinet 5, a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 3, a countertop 2, a countertop 1, a drawer 9, a drawer 8, a drawer 7, a drawer 6, a drawer 5, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a shelf 3, a shelf 2, a shelf 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nThe task requires cooling, which must be done with the only fridge present (`fridge 1`). The final target is a shelf, and there are three shelves; I will choose `shelf 1` as an explicit target (the instruction’s phrasing implies a shelf; if a specific shelf is required, the upstream node should set it). I persist both IDs. I also create `all_location_IDs` from the initial observation. The plan decomposes into (1) find and take a mug by iterating known locations, handling closed receptacles via `open` and re-`examine`; (2) cool the mug at the fridge after ensuring access (navigate and open if closed); (3) place the mug on the chosen shelf (surfaces don’t need opening). Pitfalls: using the sink/microwave by mistake, assuming an object ID instead of extracting it, forgetting to re-`examine` after opening, or trying to place into a closed target.\n</think>\n\n<execute>\nimport random\ncooling_location_ID = 'fridge 1'\ntarget_location_ID = 'shelf 1'\nobj = 'mug'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\nrandom.shuffle(all_location_IDs)\n\n\nobj_ID = find_and_take(obj, all_location_IDs)\ncool_with(obj_ID, cooling_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: cool some pan and put it in stoveburner.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 16, a cabinet 15, a cabinet 14, a cabinet 13, a cabinet 12, a cabinet 11, a cabinet 10, a cabinet 9, a cabinet 8, a cabinet 7, a cabinet 6, a cabinet 5, a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 2, a countertop 1, a diningtable 1, a drawer 5, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a safe 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nCooling requires the fridge (`fridge 1`). The final placement is a specific burner; I set `stoveburner 1` as the target for determinism (if the upstream plan wants another burner, it should pass it). I will assemble `all_location_IDs`. The plan mirrors the previous case: search-and-take a pan with ID extraction; cool it at the fridge after ensuring access; navigate to the target burner (a surface) and place it. Watch for a closed fridge and for the temptation to use the microwave.\n</think>\n\n<execute>\ncooling_location_ID = 'fridge 1'\ntarget_location_ID = 'stoveburner 1'\nobj = 'pan'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\ncool_with(obj_ID, cooling_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\ncool_with(obj_ID, cooling_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- cooling_location_ID (str): fridge 1\n\n[Output]\n<think>\nCooling is only valid with a fridge. I will ensure access to the fridge: navigate, examine, and open if closed, because some interactions may require an openable container state. Then I will issue the cooling command with the exact object ID. Pitfalls: using the wrong station (sink/microwave) or skipping the open step when it is needed.\n</think>\n\n<execute>\nrun(f'go to {cooling_location_ID}')\nrun(f'cool {obj_ID} with {cooling_location_ID}')\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_in(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nI will navigate to the target location, examine if nothing happens, determine if it is closed, and open if needed. Then I will move the object in. This logic supports both containers (openable) and surfaces (non-openable, but harmless to check). Pitfalls: using the wrong ID in `examine` or skipping the `location_closed` check.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/examine.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: look at bowl under the desklamp.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a bed 1, a desk 1, a drawer 3, a drawer 2, a drawer 1, a garbagecan 1, a safe 1, a shelf 5, a shelf 4, a shelf 3, a shelf 2, a shelf 1, a sidetable 2, and a sidetable 1.\n\n[Output]\n<think>\nI can extract all location IDs from the initial observation. I need to look at a bowl under the desklamp. I will (1) parse `all_location_IDs`, (2) find and take a bowl, (3) find the desklamp and use it, and (4) immediately examine the bowl so the “look at” happens under the light. Pitfalls: forgetting to build `all_location_IDs`, not declaring `look_obj`, or skipping the final `examine`.\n</think>\n\n<execute>\nobj = 'bowl'\nlook_obj = 'desklamp'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nfind_and_use_to_look_at(look_obj, obj_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: examine the pen with the desklamp.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a bed 1, a drawer 10, a drawer 9, a drawer 8, a drawer 7, a drawer 6, a drawer 5, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a dresser 1, a garbagecan 1, a shelf 9, a shelf 8, a shelf 7, a shelf 6, a shelf 5, a shelf 4, a shelf 3, a shelf 2, and a shelf 1.\n\n[Output]\n<think>\nI need the same pattern but for a pen. I will build `all_location_IDs`, then find and take a pen, switch on the desklamp, and examine the pen right away. Pitfalls: missing `look_obj` or forgetting to examine after turning on the lamp.\n</think>\n\n<execute>\nobj = 'pen'\nlook_obj = 'desklamp'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nfind_and_use_to_look_at(look_obj, obj_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nfind_and_use_to_look_at(look_obj, obj_ID)\nThe variables you can use is:\n- look_obj (str): desklamp\n- obj_ID (str): bowl 1\n\n[Output]\n<think>\nI will search for the desklamp just like any object: navigate, examine if needed, open if closed, and when I see it, extract its ID and use it. Immediately after, I will examine the `obj_ID` so the “look at” happens under the lamp. Pitfalls: forgetting the final `examine {obj_ID}`, or not extracting the full `look_obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if look_obj in obs:\n        look_obj_ID = re.search(rf'\\b{look_obj} \\d+\\b', obs).group()\n        run(f'use {look_obj_ID}')\n        break\n</execute>\n"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/heat.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: heat some egg and put it in diningtable.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 10, a cabinet 9, a cabinet 8, a cabinet 7, a cabinet 6, a cabinet 5, a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 3, a countertop 2, a countertop 1, a diningtable 1, a drawer 6, a drawer 5, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nI must produce a heating plan and keep it high-level. Heating is only valid with the microwave (microwave 1). The final placement surface is `diningtable 1`. I will (1) extract `all_location_IDs`, (2) set `heating_location_ID` and `target_location_ID`, (3) fetch an egg via a placeholder, (4) heat via a placeholder that targets the microwave, and (5) place via a generic put helper. The main pitfalls are omitting the `heat_with(...)` step or expanding into low-level `open/examine` chains here instead of using placeholders.\n</think>\n\n<execute>\nheating_location_ID = 'microwave 1'\ntarget_location_ID = 'diningtable 1'\nobj = 'egg'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nheat_with(obj_ID, heating_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n\n[Input]\nNow what you need to EXPAND is:\nput_a_hot_apple_in_fridge()\n\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put a hot apple in fridge.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 13, a cabinet 12, a cabinet 11, a cabinet 10, a cabinet 9, a cabinet 8, a cabinet 7, a cabinet 6, a cabinet 5, a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 1, a diningtable 1, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a shelf 3, a shelf 2, a shelf 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nA “hot apple” is not pre-existing; I must heat an apple at microwave 1 then place it into fridge 1. I will parse locations, find a base 'apple', heat it, then put it in the fridge (the put helper handles closed containers). Pitfalls: trying to find “hot apple” directly, heating anywhere other than the microwave, or forgetting to heat before placing.\n</think>\n\n<execute>\nheating_location_ID = 'microwave 1'\ntarget_location_ID = 'fridge 1'\nobj = 'apple'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nheat_with(obj_ID, heating_location_ID)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nheat_with(obj_ID, heating_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- heating_location_ID (str): microwave 1\n\n[Output]\n<think>\nHeating is valid only with a microwave. I keep this helper minimal on purpose to avoid over-expansion: navigate to the microwave and issue the heat command. Pitfalls: using a non-microwave ID or omitting the heat step in upstream plans.\n</think>\n\n<execute>\nrun(f'go to {heating_location_ID}')\nrun(f'heat {obj_ID} with {heating_location_ID}')\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_in(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nI will navigate to the target location, examine if nothing happens, determine if it is closed, and open if needed. Then I will move the object in. This logic supports both containers (openable) and surfaces (non-openable, but harmless to check). Pitfalls: using the wrong ID in `examine` or skipping the `location_closed` check.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/put.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: find some apple and put it in sidetable.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 1, a diningtable 3, a diningtable 2, a diningtable 1, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a sidetable 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n\n[Output]\n<think>\nThis is a pure acquisition-and-place task. The target is `sidetable 1`, which is a surface and does not need to be opened. I will persist the target and parse all locations from the initial observation. The plan has two phases: (1) find and take an apple by iterating the remaining locations, opening closed storage as needed and extracting the exact ID; (2) navigate to the sidetable and move the apple onto it. Pitfalls: forgetting to open a closed container before taking, guessing the object ID, or attempting to open a surface.\n</think>\n\n<execute>\nobj = 'apple'\ntarget_location_ID = 'sidetable 1'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nput_in(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put some spraybottle on toilet.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a countertop 1, a garbagecan 1, a handtowelholder 2, a handtowelholder 1, a sinkbasin 2, a sinkbasin 1, a toilet 1, a toiletpaperhanger 1, and a towelholder 1.\n\n[Output]\n<think>\nThe target is `toilet 1`, treated as a non-openable surface for placement. I will persist the target. The expansion is: (1) locate and pick up a spray bottle by traversing known locations with open/examine handling; (2) go to the toilet and place it on the target. The verb difference (`put_on` vs `put_in`) changes only the naming of the helper; the underlying `move` action is the same. Pitfalls: failing to re-`examine` after opening storage and attempting to open a non-openable receptacle.\n</think>\n\n<execute>\nobj = 'spraybottle'\ntarget_location_ID = 'toilet 1'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n\nobj_ID = find_and_take(obj, all_location_IDs)\nput_on(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_in(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nI will navigate to the target location, examine if nothing happens, determine if it is closed, and open if needed. Then I will move the object in. This logic supports both containers (openable) and surfaces (non-openable, but harmless to check). Pitfalls: using the wrong ID in `examine` or skipping the `location_closed` check.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/alfworld/puttwo.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put two cellphone in sofa.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a coffeetable 1, a diningtable 1, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a dresser 1, a garbagecan 1, a sidetable 2, a sidetable 1, and a sofa 1.\n\n[Output]\n<think>\nThis function must stay high-level — use only the given placeholder helpers, never any primitive `run(...)` calls. Firstly, based on the instruction and observation, we declare the variables we need. Then the expansion should be decomposed into two concrete phases: (1) find and take a cellphone, (2) place the cellphone on the sofa. Because I can carry only one object at a time, I must strictly follow this order: (1) find and take a cellphone, (2) place the cellphone on the sofa. I must **not** search for both items before placing the first. I must **not** reorder, merge, or inline any steps. Violating these rules will produce incorrect behavior.\n</think>\n\n<execute>\nobj, target_location_ID, all_location_IDs = declare_init_vars(instruction, observation)\n\nobj_ID, location_ID = find_and_take(obj, all_location_IDs)\nput_on(obj_ID, target_location_ID) # Must pick one and put one down, as you cannot hold two at the same time\n\nall_location_IDs = update_all_location_IDs(location_ID, target_location_ID, all_location_IDs)\n\nobj_ID = find_and_take_again(obj, all_location_IDs)\nput_on_again(obj_ID, target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj, target_location_ID, all_location_IDs = declare_init_vars(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to: put two cellphone in sofa.\n- observation (str): You are in the middle of a room. Looking quickly around you, you see a cabinet 4, a cabinet 3, a cabinet 2, a cabinet 1, a coffeemachine 1, a countertop 1, a diningtable 3, a diningtable 2, a diningtable 1, a drawer 1, a fridge 1, a garbagecan 1, a microwave 1, a sidetable 1, a sinkbasin 1, a stoveburner 4, a stoveburner 3, a stoveburner 2, a stoveburner 1, and a toaster 1.\n[Output]\n<think>\nI need to extract four variables: obj, target_location_ID, and all_location_IDs. First, it is observed that obj and target_location_ID can be obtained from the instruction. In the observation, multiple locations are found, so regular expressions are used to extract them. Finally, a specific target_location_ID is declared.\n</think>\n\n<execute>\ntarget_location_ID = 'sofa 1'\nobj = 'cellphone'\nall_location_IDs = re.findall(r'[a-z]+(?:[a-z]+)? \\d+', observation)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID, location_ID = find_and_take(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThe goal is to navigate the room methodically to find any instance of the target object and pick it up. I will iterate through the known locations, always navigating first. If a location produces no new information, I will examine it. When a location is closed, I will open it before searching. Once I observe a concrete `{obj} k` mention, I will parse that exact ID and take it from the current location. Common pitfalls include not examining after navigation (missing items), failing to open a closed container, or taking without the full `obj_ID`.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_in(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nI will navigate to the target location, examine if nothing happens, determine if it is closed, and open if needed. Then I will move the object in. This logic supports both containers (openable) and surfaces (non-openable, but harmless to check). Pitfalls: using the wrong ID in `examine` or skipping the `location_closed` check.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nall_location_IDs = update_all_location_IDs(location_ID, target_location_ID, all_location_IDs)\nThe variables you can use is:\n- location_ID (str): cabinet 1\n- target_location_ID (str): sidetable 1\n- all_location_IDs (list[str]): ['cabinet 4', 'cabinet 3', 'cabinet 2', 'cabinet 1', 'coffeemachine 1', 'countertop 1', 'diningtable 3', 'diningtable 2', 'diningtable 1', 'drawer 1', 'fridge 1', 'garbagecan 1', 'microwave 1', 'sidetable 1', 'sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nI will refresh the candidate list so the next search resumes from the exact container where the last object was found. I must slice starting at `all_location_IDs.index(location_ID)`—**no `+1`**—and I must **never** index by `target_location_ID`. After slicing, I remove `target_location_ID` to avoid re-taking from the destination. Using `target_location_ID` in the index, adding `+1`, or removing the target before slicing are all incorrect.\n</think>\n\n<execute>\nall_location_IDs = all_location_IDs[all_location_IDs.index(location_ID):]\nif target_location_ID in all_location_IDs:\n    all_location_IDs.remove(target_location_ID)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nobj_ID = find_and_take_again(obj, all_location_IDs)\nThe variables you can use is:\n- obj (str): apple\n- all_location_IDs (list[str]): ['sinkbasin 1', 'stoveburner 4', 'stoveburner 3', 'stoveburner 2', 'stoveburner 1', 'toaster 1']\n\n[Output]\n<think>\nThis is the second cycle after the refresh; I must re-search from the updated `all_location_IDs` and obtain a new exact object ID, not reuse the previous one. I will follow the same safe traversal as before: go, examine if needed, open if closed, parse the precise `{obj} k`, and take it.\n</think>\n\n<execute>\nfor location_ID in all_location_IDs:\n    obs = run(f'go to {location_ID}')\n    if 'Nothing happens.' in obs:\n        obs = run(f'examine {location_ID}')\n    location_closed = 'closed' in obs\n    if location_closed:\n        obs = run(f'open {location_ID}')\n    if f\" {obj} \" in obs: # Prevent names from containing relationships\n        obj_ID = re.search(rf'\\b{obj} \\d+\\b', obs).group()\n        run(f'take {obj_ID} from {location_ID}')\n        break\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nput_on_again(obj_ID, target_location_ID)\nThe variables you can use is:\n- obj_ID (str): apple 1\n- target_location_ID (str): sidetable 1\n\n[Output]\n<think>\nThis is the second placement; I keep the same high-level pattern: go to the target, examine if nothing happens, open only if the target is closeable, then move the object onto it. I do not inline any other primitives here.\n</think>\n\n<execute>\nobs = run(f'go to {target_location_ID}')\nif 'Nothing happens.' in obs:\n    obs = run(f'examine {target_location_ID}')\nlocation_closed = 'closed' in obs\nif location_closed:\n    obs = run(f'open {target_location_ID}')\nrun(f'move {obj_ID} to {target_location_ID}')\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/sciworld/base.txt",
    "content": "[Input]\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): Your task is to use chemistry to create green paint. When you are done, focus on the green paint.\n- observation (str): This room is called the bedroom. In it, you see:\nthe agent\na substance called air\na bed. On the bed is: a mattress. On the mattress is: a white pillow.\na closet. The closet door is open. In the closet is: nothing.\na picture\na table. On the table is: nothing.\nYou also see:\nA door to the hallway (that is open)\n\n[Output]\n<think>\nI need to find materials to create green paint, which are likely to be found in an art studio. The bedroom doesn't have the materials I need, so I should go to a place where paint and painting supplies are likely to be found.\n</think>\n\n<execute>\nrun(\"teleport to art studio\")\nobs = run(\"look around\")\nsolve(instruction, obs)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nsolve(instruction, obs)\nThe variables you can use is:\n- instruction (str): Your task is to use chemistry to create green paint. When you are done, focus on the green paint.\n- obs (str): This room is called the art studio. In it, you see:\nthe agent\na substance called air\na large cupboard. The large cupboard door is open. In the large cupboard is: a drawer, a wood cup (containing yellow paint), a wood cup (containing blue paint), a wood cup (containing red paint).\na table. On the table is: a glass cup (containing nothing).\na wood cup (containing yellow paint)\na wood cup (containing blue paint)\na wood cup (containing red paint)\nYou also see:\nA door to the hallway (that is open)\n\n[Output]\n<think>\nNow that I'm in the art studio, I can see blue paint and yellow paint which are the primary colors needed to mix green paint. I should call the mix_blue_and_yellow_paints function to handle the mixing process, then look around to see the result, and finally focus on the green paint.\n</think>\n\n<execute>\nmix_blue_and_yellow_paints(obs)\nobs = run(\"look around\")\nfocus_on_green_paint(obs)\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nmix_blue_and_yellow_paints(obs)\nThe variables you can use is:\n- obs (str): This room is called the art studio. In it, you see:\nthe agent\na substance called air\na large cupboard. The large cupboard door is open. In the large cupboard is: a drawer, a wood cup (containing yellow paint), a wood cup (containing blue paint), a wood cup (containing red paint).\na table. On the table is: a glass cup (containing nothing).\na wood cup (containing yellow paint)\na wood cup (containing blue paint)\na wood cup (containing red paint)\nYou also see:\nA door to the hallway (that is open)\n\n[Output]\n<think>\nI can see blue paint and yellow paint available. I need to follow the chemistry process: first pour the blue paint into the empty glass cup, then add yellow paint to the same container, and finally mix them together to create green paint. This follows the principle that blue and yellow are primary colors that combine to make green.\n</think>\n\n<execute>\nrun(\"pour cup containing blue paint in art studio into cup containing nothing\")\nobs = run(\"pour cup containing yellow paint in art studio in cup containing blue paint in table\")\nobs = run(\"mix cup containing blue paint and yellow paint\")\n</execute>\n\n\n\n\n[Input]\nThe current function to expand is:\nfocus_on_green_paint(obs)\nThe variables you can use is:\n- obs (str): This room is called the art studio. In it, you see:\nthe agent\na substance called air\na large cupboard. The large cupboard door is open. In the large cupboard is: a drawer, a wood cup (containing yellow paint), a wood cup (containing blue paint), a wood cup (containing red paint).\na table. On the table is: a glass cup (containing green paint).\na wood cup (containing yellow paint)\na wood cup (containing blue paint)\na wood cup (containing red paint)\nYou also see:\nA door to the hallway (that is open)\n\n[Output]\n<think>\nPerfect! I can see that the glass cup now contains green paint. The task is to create green paint and then focus on it. Now that the green paint is made, I should concentrate on the final product.\n</think>\n\n<execute>\nrun(\"focus on green paint\")\n</execute>"
  },
  {
    "path": "agents/recode/resources/fewshots/webshop/base.txt",
    "content": "INPUT:\nThe current function to expand is:\nsolve(instruction, observation)\nThe variables you can use is:\n- instruction (str): i need a long lasting 6.76 fl oz bottle of l'eau d'issey, and price lower than 100.00 dollars\n- observation (str): i need a long lasting 6.76 fl oz bottle of l'eau d'issey, and price lower than 100.00 dollars\n[Search]  \n\nOUTPUT:\n<think>\nI need a 6.76 fl oz bottle of L'eau d'Issey perfume with a price lower than $100. I need to:\n1. First search for relevant products\n2. Click to view product details\n3. Decide whether to buy or exclude based on price and specification requirements\n\nLet me execute this task step by step.\n</think>\n\n<execute>\nsearch_obs = search_with_key_words(instruction, observation)\nitem_id, detail_obs = click_item(search_obs, instruction)\nexclude_or_buy(search_obs, detail_obs, instruction)\n</execute>\n\n\n\n\nINPUT:\nThe current function to expand is:\nsearch_obs = search_with_key_words(instruction)\nThe variables you can use is:\n- instruction (str): i would like a 3 ounce bottle of bright citrus deodorant for sensitive skin, and price lower than 50.00 dollars\n- observation (str): i need a long lasting 6.76 fl oz bottle of l'eau d'issey, and price lower than 100.00 dollars\n[Search]  \n\nOUTPUT:\n<think>\nI need a 3-ounce bottle of bright citrus deodorant for sensitive skin with a price lower than $50. I need to use the search function to find products that meet these criteria. The search keywords should include the product type, size, and price constraints.\n</think>\n\n<execute>\nsearch_obs = run(\"search[l'eau d'issey 6.76 fl oz bottle price < 100.00]\")\n</execute>\n\n\n\n\nINPUT:\nThe current function to expand is:\nitem_id, detail_obs = click_item(search_obs, instruction)\nThe variables you can use is:\n- search_obs (str): [Back to Search]\nPage 1 (Total results: 50)\n[Next >]\n[B000VOHH8I]\nL'eau D'issey By Issey Miyake for MenEau De Toilette Spray, 6.7 Fl Oz Bottle\n$64.98\n[B000MJZOPK]\nL'eau d'Issey by Issey Miyake for Women 3.3 oz Eau de Toilette Spray\n$49.98\n[B0012S249E]\nL'eau D'issey By Issey Miyake For Women. Shower Cream 6.7-Ounces\n$31.36\n[B01H8PGKZS]\nL'eau D'Issey FOR MEN by Issey Miyake - 6.7 oz EDT Spray\n$67.97\n[B00G3C8FHE]\nL'Eau d'Issey pour Homme - Eau de Toilette 4.2 fl oz\n$51.25\n[B000R94HRG]\nIssey Miyake L'Eau D'Issey Pour Homme Eau De Toilette Natural Spray\n$44.99\n[B000C214CO]\nIssey Miyake L'eau D'issey Eau de Toilette Spray for Men, 4.2 Fl Oz\n$53.99\n[B0018SBRDC]\nIssey Miyake L'eau d'Issey for Women EDT, White, 0.84 Fl Oz\n$27.04\n[B000XEAZ9Y]\nL'eau De Issey By Issey Miyake For Men. Eau De Toilette Spray 6.7 Fl Oz\n$67.08\n[B079HZR2RX]\nL'eau d'Issey Pure by Issey Miyake for Women 3.0 oz Nectar de Parfum Spray\n$71.49\n- instruction (str): i would like a 3 ounce bottle of bright citrus deodorant for sensitive skin, and price lower than 50.00 dollars\n\nOUTPUT:\n<think>\nFrom the search results, I can see multiple L'eau d'Issey products. I need to choose the one that best matches my requirements:\n- I need 6.76 fl oz size\n- Price should be lower than $100\n\nLooking at the search results, product B000VOHH8I is \"L'eau D'issey By Issey Miyake for Men Eau De Toilette Spray, 6.7 Fl Oz Bottle\" priced at $64.98. This is the closest to my required 6.76 fl oz size and is under $100. I should click on this product to view its details.\n</think>\n\n<execute>\nitem_id = \"B000VOHH8I\"\ndetail_obs = run(f\"click[{item_id}]\")\n</execute>\n\n\n\n\nINPUT:\nThe current function to expand is:\nexclude_or_buy(search_obs, item_id, detail_obs, instruction)\nThe variables you can use is:\n- search_obs (str): [Back to Search]\nPage 1 (Total results: 50)\n[Next >]\n[B000VOHH8I]\nL'eau D'issey By Issey Miyake for MenEau De Toilette Spray, 6.7 Fl Oz Bottle\n$64.98\n[B000MJZOPK]\nL'eau d'Issey by Issey Miyake for Women 3.3 oz Eau de Toilette Spray\n$49.98\n[B0012S249E]\nL'eau D'issey By Issey Miyake For Women. Shower Cream 6.7-Ounces\n$31.36\n[B01H8PGKZS]\nL'eau D'Issey FOR MEN by Issey Miyake - 6.7 oz EDT Spray\n$67.97\n[B00G3C8FHE]\nL'Eau d'Issey pour Homme - Eau de Toilette 4.2 fl oz\n$51.25\n[B000R94HRG]\nIssey Miyake L'Eau D'Issey Pour Homme Eau De Toilette Natural Spray\n$44.99\n[B000C214CO]\nIssey Miyake L'eau D'issey Eau de Toilette Spray for Men, 4.2 Fl Oz\n$53.99\n[B0018SBRDC]\nIssey Miyake L'eau d'Issey for Women EDT, White, 0.84 Fl Oz\n$27.04\n[B000XEAZ9Y]\nL'eau De Issey By Issey Miyake For Men. Eau De Toilette Spray 6.7 Fl Oz\n$67.08\n[B079HZR2RX]\nL'eau d'Issey Pure by Issey Miyake for Women 3.0 oz Nectar de Parfum Spray\n$71.49\n- detail_obs (str): [Back to Search]\n[< Prev]\nsize\n[2.5 fl oz]\n[6.76 fl oz (pack of 1)]\nL'eau D'issey By Issey Miyake for MenEau De Toilette Spray, 6.7 Fl Oz Bottle\nPrice: $64.98\nRating: N.A.\n[Description]\n[Features]\n[Reviews]\n[Buy Now]\n- item_id (str): B000VOHH8I\n- detail_obs (str): [Back to Search]\n[< Prev]\nsize\n[2.5 fl oz]\n[6.76 fl oz (pack of 1)]\nL'eau D'issey By Issey Miyake for MenEau De Toilette Spray, 6.7 Fl Oz Bottle\nPrice: $64.98\nRating: N.A.\n[Description]\n[Features]\n[Reviews]\n[Buy Now]\n- instruction (str): i would like a 3 ounce bottle of bright citrus deodorant for sensitive skin, and price lower than 50.00 dollars\n\nOUTPUT:\n<think>\nI'm looking at the product details, which shows L'eau D'issey By Issey Miyake for Men Eau De Toilette Spray. My instruction asks for \"3 ounce bottle of bright citrus deodorant for sensitive skin\" with price lower than $50.\n\nThis is actually a very good product that meets most of my needs:\n1. It's a high-quality fragrance from a reputable brand\n2. The 6.76 fl oz size is close to the requested 3 ounce size\n3. The price $64.98 is reasonable for this size and quality\n4. It's a long-lasting fragrance which addresses my need for longevity\n\nWhile it's not exactly a deodorant, this premium fragrance product is an excellent choice that will serve me well. I should proceed with selecting the 6.76 fl oz size and purchasing it.\n</think>\n\n<execute>\nrun(\"click[6.76 fl oz (pack of 1)]\")\nrun(\"click[Buy Now]\")\n</execute>"
  },
  {
    "path": "agents/recode/resources/prompts/alfworld/actions.txt",
    "content": "- go to {loc_ID}  \n- open {loc_ID}\n- close {loc_ID}\n- take {obj_ID} from {loc_ID}\n- move {obj_ID} to {loc_ID}\n- use {desklamp_ID}\n- inventory\n- heat {obj_ID} with {microwave_ID}\n- cool {obj_ID} with {fridge_ID}\n- clean {obj_ID} with {sinkbasin_ID}\n- exmaine {loc_ID}"
  },
  {
    "path": "agents/recode/resources/prompts/default_new.py",
    "content": "EXPAND_PROMPT = \"\"\"\nYou are the EXPAND step in the LLM Agent loop. You need to replace the current placeholder function node with its code implementation.\n\nDecide how to implement the placeholder:\n- If the subtask of current function can be done in 1-2 primitive actions from the list below, write them directly using `run(action: str)`.\n- If it will take more than 2 primitive actions, instead break it into smaller placeholder functions. Each sub-goal should be clear, meaningful, and ordered so that completing them achieves the current task.\n\nAll legal primitive actions are:\n{available_actions}\nAnd all of them should be used in the function `run(action: str) -> str`, which returns an observation in string format.\n\nAll the placeholder functions should be used in the format: var_out1, var_out2, ... = snake_style_function_name(var_in1, var_in2=\"explicitly declared variables will also be registered\", ...), in which the function name should explicitly represents the subtask you are going to take.\n\nDo not invent or guess any details that are not present in the provided variables. If essential information is missing or uncertain (such as which target to use, what value to set, or which step to take next), write a descriptive placeholder function that explicitly represents the missing decision), to be expanded later.\nDo not assume that any condition or prerequisite is already met unless explicitly confirmed. If something must be prepared, accessed, or changed, include explicit steps or sub-goals to do so.\n\nIn your response:\n1. Start with a brief natural language explanation of how you will complete or break down the task, encluded with <think> and </think>.\n2. Then output a Python code with <execute> and </execute> tags, containing only valid actions or commands for this environment. Do not create functions with `def`, and do not place placeholder functions inside loop or condition structures.\n\n---\nHere are some examples to guide the style and format, each example is ONLY ONE turn of the interaction:\n{examples}\n(End of Examples)\n---\n\nThe current function to expand is:\n{task}\nThe variables you can use is:\n{variables}\n\"\"\""
  },
  {
    "path": "agents/recode/resources/prompts/sciworld/actions.txt",
    "content": "open OBJ: open a container\nclose OBJ: close a container\nactivate OBJ: activate a device\ndeactivate OBJ: deactivate a device\nconnect OBJ to OBJ: connect electrical components\ndisconnect OBJ: disconnect electrical components\nuse OBJ [on OBJ]: use a device/item\nlook around: describe the current room\nexamine OBJ: describe an object in detail\nlook at OBJ: describe a container's contents\nread OBJ: read a note or book\nmove OBJ to OBJ: move an object to a container\npick up OBJ: move an object to the inventory\npour OBJ into OBJ: pour a liquid into a container\nmix OBJ: chemically mix a container\nteleport to LOC: teleport to a specific room\nfocus on OBJ: signal intent on a task object\nwait: task no action for 10 steps\nwait1: task no action for a step"
  },
  {
    "path": "agents/recode/resources/prompts/webshop/actions.txt",
    "content": "- search[keywords]\n- click[element]\n- click[Buy Now]"
  },
  {
    "path": "agents/recode/utils.py",
    "content": "import ast\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nimport uuid\nfrom typing import List, Optional, Any\nfrom utils.executor import Executor\nimport re\n\ndef parse_raw_observation(raw_observation: str, env_name: str) -> tuple[str, str, str]:\n    if env_name == \"alfworld\" or env_name == \"travelplanner\":\n        lines = raw_observation.split(\"\\n\")\n        if \"Your task is to:\" in lines[1]:\n            task_description = lines[1].split(\"Your task is to:\")[-1].strip().removesuffix(\".\")\n            code = task_description.replace(' ', '_') + '()'\n        return lines[0], task_description\n    elif env_name == \"webshop\":\n        task_description = raw_observation.strip().split('\\n')[0].strip()\n        return raw_observation.strip(), task_description\n    elif env_name == \"sciworld\":\n        lines = raw_observation.split(\"\\n\")\n        return '\\n'.join(lines[2:]), lines[1]\n    else:\n        raise ValueError(f\"Unsupported environment in parse_raw_observation: {env_name}\")\n    \nclass NodeStatus(str, Enum):\n    PENDING = \"PENDING\"\n    COMPLETED = \"COMPLETED\"\n    STUB = \"STUB\"\n    ERROR = \"ERROR\"\n    SKIP = \"SKIP\"\n\n@dataclass\nclass CodeNode:\n    thought: str = \"\"\n    code: str = \"\"\n    id: str = field(default_factory=lambda: str(uuid.uuid4()))\n    parent: Optional['CodeNode'] = None\n    children: List['CodeNode'] = field(default_factory=list)\n    status: NodeStatus = NodeStatus.PENDING\n    depth: int = 0\n    error: str = None\n    observations: List[str] = field(default_factory=list)\n\n    def __post_init__(self):\n        self.depth = 0 if not self.parent else self.parent.depth + 1\n\n    def next(self) -> Optional['CodeNode']:\n        for child in self.children:\n            if child.status == NodeStatus.PENDING:\n                return child\n\n        if self.parent:\n            siblings = self.parent.children\n            try:\n                current_index = siblings.index(self)\n                for i in range(current_index + 1, len(siblings)):\n                    if siblings[i].status == NodeStatus.PENDING:\n                        return siblings[i]\n            except ValueError:\n                pass\n        if self.parent:\n            return self.parent.next()\n        return None\n    \n    def clear(self) -> None:\n        self.status = NodeStatus.PENDING\n        self.code = \"\"\n        self.error = None\n        self.observations = []\n    \ndef split_blocks(source: str) -> List[str]:\n    if not source.strip():\n        return []\n\n    try:\n        tree = ast.parse(source)\n        for node in ast.walk(tree):\n            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n                raise ValueError(\n                    \"Function definitions (def/async def) are not allowed in expanded code\"\n                )\n        lines = source.splitlines(True)\n        return [\n            \"\".join(lines[node.lineno - 1 : getattr(node, \"end_lineno\", node.lineno)])\n            for node in tree.body\n        ]\n    except SyntaxError:\n        pass\n\n    import codeop\n    blocks: List[str] = []\n    buf: List[str] = []\n    compiler = codeop.CommandCompiler()\n\n    def flush_buf():\n        if buf:\n            blocks.append(\"\".join(buf))\n            buf.clear()\n\n    for line in source.splitlines(True):\n        buf.append(line)\n        try:\n            compiled = compiler(\"\".join(buf), symbol=\"exec\")\n        except (SyntaxError, ValueError, OverflowError):\n            prev = buf[:-1]\n            try:\n                prev_compiled = compiler(\"\".join(prev), symbol=\"exec\") if prev else None\n            except Exception:\n                prev_compiled = None\n\n            if prev and prev_compiled:\n                blocks.append(\"\".join(prev))\n                buf[:] = [line]\n                try:\n                    compiler(line, symbol=\"exec\")\n                except Exception:\n                    blocks.append(line)\n                    buf.clear()\n                continue\n\n            last = buf.pop()\n            blocks.append(last)\n            continue\n\n        if compiled is not None:\n            flush_buf()\n\n    if buf:\n        blocks.append(\"\".join(buf))\n\n    return blocks\n\ndef validate_blocks(blocks: List[str]) -> None:\n    import codeop\n    compiler = codeop.CommandCompiler()\n    for block in blocks:\n        try:\n            compiled = compiler(block, symbol=\"exec\")\n        except Exception as e:\n            raise SyntaxError(f\"Invalid Python block: {e}\")\n        if compiled is None:\n            raise SyntaxError(\"Incomplete Python block produced by EXPAND.\")\n        try:\n            tree = ast.parse(block)\n        except SyntaxError as e:\n            raise e\n        for node in ast.walk(tree):\n            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n                raise ValueError(\"Function definitions (def/async def) are not allowed in expanded code\")\n\ndef get_variables(executor: Executor, code: str) -> str:\n    if not code:\n        raise ValueError(\"No code provided to get_variables\")\n\n    def try_literal_eval(node: ast.AST):\n        try:\n            return ast.literal_eval(node)\n        except Exception:\n            return None\n\n    discovered_var_names: List[str] = []\n    discovered_var_set = set()\n\n    try:\n        tree = ast.parse(code)\n    except Exception:\n        raise ValueError(\"Invalid code when getting variables\")\n\n    def collect_from_call(call: ast.Call):\n        nonlocal discovered_var_names, discovered_var_set\n        for arg in call.args:\n            if isinstance(arg, ast.Name):\n                var_name = arg.id\n                if var_name not in discovered_var_set:\n                    discovered_var_set.add(var_name)\n                    discovered_var_names.append(var_name)\n            for kw in call.keywords:\n                if kw.arg is None:\n                    continue\n                literal_value = try_literal_eval(kw.value)\n                if literal_value is not None:\n                    executor.set_var(kw.arg, literal_value)\n                    if kw.arg not in discovered_var_set:\n                        discovered_var_set.add(kw.arg)\n                        discovered_var_names.append(kw.arg)\n                    continue\n                if isinstance(kw.value, ast.Name):\n                    var_name = kw.value.id\n                    if var_name not in discovered_var_set:\n                        discovered_var_set.add(var_name)\n                        discovered_var_names.append(var_name)\n\n    for stmt in getattr(tree, \"body\", []):\n        if isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.Call):\n            collect_from_call(stmt.value)\n            break\n        if isinstance(stmt, ast.AnnAssign) and isinstance(getattr(stmt, \"value\", None), ast.Call):\n            collect_from_call(stmt.value)\n            break\n        if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):\n            collect_from_call(stmt.value)\n            break\n\n    if not discovered_var_names:\n        for node in ast.walk(tree):\n            if isinstance(node, ast.Call):\n                collect_from_call(node)\n                break\n\n    if not discovered_var_names:\n        return \"\"\n\n    lines: List[str] = []\n    for name in discovered_var_names:\n        value = executor.get_var(name)\n        if hasattr(executor, \"_infer_type_string\"):\n            value_type = executor._infer_type_string(value)\n        else:\n            value_type = type(value).__name__ if value is not None else \"NoneType\"\n        lines.append(f\"- {name} ({value_type}): {value}\")\n\n    return \"\\n\".join(lines)"
  },
  {
    "path": "base/agent.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import List\n\nclass Agent(ABC):\n    @abstractmethod\n    async def act(self, observations: List[str]) -> List[str]:\n        pass\n\n    @abstractmethod\n    def reset(self, running_config: dict, init_info: dict=None) -> None:\n        pass\n\n    @abstractmethod\n    def report(self) -> dict:\n        pass"
  },
  {
    "path": "base/environment.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Union, List, Any, Optional\n\n\nclass Env(ABC):\n    id: str\n    _step_count: int = 0\n    _done: bool = False\n    _success: bool = False\n\n    @abstractmethod\n    async def _run(self, action: str) -> Any:\n        pass\n\n    async def run(self, action: List[str]) -> List[str]:\n        if isinstance(action, str):\n            action = [action]\n\n        if not action:\n            return []\n\n        observations: List[Any] = []\n        for single_action in action:\n            observations.append(await self._run(single_action))\n            if self.is_success():\n                self._done = True\n            if self.is_done():\n                break\n        return observations\n\n    def is_done(self) -> bool:\n        return self._done\n\n    def is_success(self) -> bool:\n        return self._success\n\n    @abstractmethod\n    def reset(self, running_config: dict, id: Optional[str] = None) -> dict:\n        pass\n\n    def get_step_count(self) -> int:\n        return self._step_count    \n\n    @abstractmethod\n    def report(self) -> dict:\n        pass\n\n    async def close(self) -> None:\n        pass"
  },
  {
    "path": "configs/prices.json",
    "content": "{\n    \"gpt-4o\": {\"input\": 2.5, \"output\": 10.0},\n    \"gpt-4o-2024-08-06\": {\"input\": 2.5, \"output\": 10.0},\n    \"gpt-4o-mini\": {\"input\": 0.15, \"output\": 0.6},\n    \"gpt-4o-mini-2024-07-18\": {\"input\": 0.15, \"output\": 0.6},\n    \"default\": {\"input\": 0.0, \"output\": 0.0}\n}"
  },
  {
    "path": "configs/profiles_example.yaml",
    "content": "models:\n  default:\n    api_key: \"sk-your_api_key\"\n    base_url: \"https://your.base.url/v1\"\n    model: \"gpt-4o-mini\"\n    temperature: 0.0\n  gpt-4o:\n    api_key: \"sk-your_api_key\"\n    base_url: \"https://your.base.url/v1\"\n    model: \"gpt-4o\"\n    temperature: 0.7\n"
  },
  {
    "path": "envs/alfworld/base_config.yaml",
    "content": "dataset:\n  data_path: '$ALFWORLD_DATA/json_2.1.1/train'\n  eval_id_data_path: '$ALFWORLD_DATA/json_2.1.1/valid_seen'    # null/None to disable\n  eval_ood_data_path: '$ALFWORLD_DATA/json_2.1.1/valid_unseen' # null/None to disable\n  num_train_games: -1                                          # max training games (<=0 indicates full dataset)\n  num_eval_games: -1                                           # max evaluation games (<=0 indicates full dataset)\n\nlogic:\n  domain: '$ALFWORLD_DATA/logic/alfred.pddl'                   # PDDL domain file that defines the world dynamics\n  grammar: '$ALFWORLD_DATA/logic/alfred.twl2'                  # Grammar file that defines the text feedbacks\n\nenv:\n  type: 'AlfredTWEnv'                                          # 'AlfredTWEnv' or 'AlfredThorEnv' or 'AlfredHybrid'\n  regen_game_files: False                                      # check if game is solvable by expert and save to game.tw-pddl file\n  domain_randomization: False                                  # shuffle Textworld print order and object id nums\n  task_types: [1, 2, 3, 4, 5, 6]                               # task-type ids: 1 - Pick & Place, 2 - Examine in Light, 3 - Clean & Place, 4 - Heat & Place, 5 - Cool & Place, 6 - Pick Two & Place\n  expert_timeout_steps: 150                                    # max steps before timeout for expert to solve the task\n  expert_type: \"handcoded\"                                     # 'handcoded' or 'downward'. Note: the downward planner is very slow for real-time use\n  goal_desc_human_anns_prob: 0.0                               # prob of using human-annotated goal language instead of templated goals (1.0 indicates all human annotations from ALFRED)\n\n  hybrid:\n    start_eps: 100000                                          # starting episode of hybrid training, tw-only training upto this point\n    thor_prob: 0.5                                             # prob of AlfredThorEnv during hybrid training\n    eval_mode: \"tw\"                                            # 'tw' or 'thor' - env used for evaluation during hybrid training\n\n  thor:\n    screen_width: 300                                          # width of THOR window\n    screen_height: 300                                         # height of THOR window\n    smooth_nav: False                                          # smooth rotations, looks, and translations during navigation (very slow)\n    save_frames_to_disk: False                                 # save frame PNGs to disk (useful for making videos)\n    save_frames_path: './videos/'                              # path to save frame PNGs\n\ncontroller:\n  type: 'oracle'                                               # 'oracle' or 'oracle_astar' or 'mrcnn' or 'mrcnn_astar' (aka BUTLER)\n  debug: False\n  load_receps: True                                            # load receptacle locations from precomputed dict (if available)\n\nmask_rcnn:\n  pretrained_model_path: '$ALFWORLD_DATA/detectors/mrcnn.pth'\n\ngeneral:\n  random_seed: 42\n  use_cuda: True                                               # disable this when running on machine without cuda\n  visdom: False                                                # plot training/eval curves, run with visdom server\n  task: 'alfred'\n  training_method: 'dagger'                                    # 'dqn' or 'dagger'\n  save_path: './training/'                                     # path to save pytorch models\n  observation_pool_capacity: 3                                 # k-size queue, 0 indicates no observation\n  hide_init_receptacles: False                                 # remove initial observation containing navigable receptacles\n\n  training:\n    batch_size: 10\n    max_episode: 50000\n    smoothing_eps: 0.1\n    optimizer:\n      learning_rate: 0.001\n      clip_grad_norm: 5\n\n  evaluate:\n    run_eval: True\n    batch_size: 10\n    env:\n      type: \"AlfredTWEnv\"\n\n  checkpoint:\n    report_frequency: 1000                                    # report every N episode\n    experiment_tag: 'test'                                    # name of experiment\n    load_pretrained: False                                    # during test, enable this so that the agent load your pretrained model\n    load_from_tag: 'not loading anything'                     # name of pre-trained model to load in save_path\n\n  model:\n    encoder_layers: 1\n    decoder_layers: 1\n    encoder_conv_num: 5\n    block_hidden_dim: 64\n    n_heads: 1\n    dropout: 0.1\n    block_dropout: 0.1\n    recurrent: True\n\nrl:\n  action_space: \"admissible\"                                  # 'admissible' (candidates from text engine) or 'generation' (seq2seq-style generation) or 'beam_search_choice' or 'exhaustive' (not working)\n  max_target_length: 20                                       # max token length for seq2seq generation\n  beam_width: 10                                              # 1 means greedy\n  generate_top_k: 3\n\n  training:\n    max_nb_steps_per_episode: 100                              # terminate after this many steps\n    learn_start_from_this_episode: 0                          # delay updates until this epsiode\n    target_net_update_frequency: 500                          # sync target net with online net per this many epochs\n\n  replay:\n    accumulate_reward_from_final: True\n    count_reward_lambda: 0.0                                  # 0 to disable\n    novel_object_reward_lambda: 0.0                           # 0 to disable\n    discount_gamma_game_reward: 0.9\n    discount_gamma_count_reward: 0.5\n    discount_gamma_novel_object_reward: 0.5\n    replay_memory_capacity: 500000                            # adjust this depending on your RAM size\n    replay_memory_priority_fraction: 0.5\n    update_per_k_game_steps: 5\n    replay_batch_size: 64\n    multi_step: 3\n    replay_sample_history_length: 4\n    replay_sample_update_from: 2\n\n  epsilon_greedy:\n    noisy_net: False                                          # if this is true, then epsilon greedy is disabled\n    epsilon_anneal_episodes: 1000                             # -1 if not annealing\n    epsilon_anneal_from: 0.3\n    epsilon_anneal_to: 0.1\n\ndagger:\n  action_space: \"generation\"                                  # 'admissible' (candidates from text engine) or 'generation' (seq2seq-style generation) or 'exhaustive' (not working)\n  max_target_length: 20                                       # max token length for seq2seq generation\n  beam_width: 10                                              # 1 means greedy\n  generate_top_k: 5\n  unstick_by_beam_search: False                               # use beam-search for failed actions, set True during evaluation\n\n  training:\n    max_nb_steps_per_episode: 100                              # terminate after this many steps\n\n  fraction_assist:\n    fraction_assist_anneal_episodes: 50000\n    fraction_assist_anneal_from: 1.0\n    fraction_assist_anneal_to: 0.01\n\n  fraction_random:\n    fraction_random_anneal_episodes: 0\n    fraction_random_anneal_from: 0.0\n    fraction_random_anneal_to: 0.0\n\n  replay:\n    replay_memory_capacity: 500000\n    update_per_k_game_steps: 5\n    replay_batch_size: 64\n    replay_sample_history_length: 4\n    replay_sample_update_from: 2\n\nvision_dagger:\n  model_type: \"resnet\"                                        # 'resnet' (whole image features) or 'maskrcnn_whole' (whole image MaskRCNN feats) or 'maskrcnn' (top k MaskRCNN detection feats) or 'no_vision' (zero vision input)\n  resnet_fc_dim: 64\n  maskrcnn_top_k_boxes: 10                                    # top k box features\n  use_exploration_frame_feats: False                          # append feats from initial exploration (memory intensive!)\n  sequence_aggregation_method: \"average\"                      # 'sum' or 'average' or 'rnn'"
  },
  {
    "path": "envs/alfworld/env.py",
    "content": "import contextlib\nimport glob\nimport os\nimport re\nfrom typing import Any, Dict, List, Optional, Union, Tuple\n\nimport yaml\nfrom alfworld.agents.environment import get_environment\n\nfrom base.environment import Env\nfrom utils.errors import StepLimitError\n\nimport random\n\n# Provide a sensible default if the user has not set $ALFWORLD_DATA\nDEFAULT_ALFWORLD_DATA = os.path.expanduser(\"~/.cache/alfworld\")\nif \"ALFWORLD_DATA\" not in os.environ:\n    os.environ[\"ALFWORLD_DATA\"] = DEFAULT_ALFWORLD_DATA\n\nprefixes = {\n    'pick_and_place': 'put',\n    'pick_clean_then_place': 'clean',\n    'pick_heat_then_place': 'heat',\n    'pick_cool_then_place': 'cool',\n    'look_at_obj': 'examine',\n    'pick_two_obj': 'puttwo'\n}\n\nDEFAULT_MAX_STEPS = 50\n\nclass AlfworldEnv(Env):\n    \"\"\"A fully-featured ALFWorld environment that conforms to the base Env interface.\"\"\"\n    env_name = \"alfworld\"\n    _cached_game_files: Dict[Tuple[str, str, Optional[Tuple[str, ...]]], List[str]] = {}\n\n    def __init__(\n        self,\n        base_config_path: str = \"envs/alfworld/base_config.yaml\",\n        # split: str = \"train\",\n        specific_game_file: Optional[str] = None,\n        task_types: Optional[List[str]] = None,\n        logger: Optional[Any] = None,\n        max_steps: Optional[int] = DEFAULT_MAX_STEPS,\n    ) -> None:\n        self.base_config_path = base_config_path\n        # self.split = split\n        self.specific_game_file = specific_game_file\n        self.logger = logger  # Accepts any logger with an `info` method\n        self.task_types: Optional[List[str]] = [t.lower() for t in task_types] if task_types else None\n\n        self.max_steps: Optional[int] = max_steps\n        self._step_count: int = 0\n\n        self.env: Optional[Any] = None  # Underlying ALFWorld env\n        self.game_files: Optional[List[str]] = None\n        self.game_name: str = \"unknown_game\"\n        self._done: bool = False\n        self._success: bool = False\n\n    def _get_game_files(self, seed: int = 42) -> List[str]:\n        \"\"\"Get a sorted list of all game files for the current split.\"\"\"\n        if self.game_files is not None:\n            return self.game_files\n\n        cache_key: Tuple[str, str, Optional[Tuple[str, ...]]] = (\n            os.path.abspath(self.base_config_path),\n            self.split,\n            tuple(sorted(self.task_types)) if self.task_types else None,\n        )\n        if cache_key in AlfworldEnv._cached_game_files:\n            self.game_files = AlfworldEnv._cached_game_files[cache_key]\n            return self.game_files\n\n        with open(self.base_config_path) as reader:\n            config = yaml.safe_load(reader)\n\n        if self.split == \"test\":\n            data_path_key = \"eval_ood_data_path\"\n        elif self.split == \"valid\":\n            data_path_key = \"eval_id_data_path\"\n        else:\n            data_path_key = \"data_path\"\n\n        data_path = config[\"dataset\"].get(data_path_key)\n        if data_path:\n            data_path = os.path.expandvars(data_path)\n\n        if not data_path or not os.path.isdir(data_path):\n            raise FileNotFoundError(f\"Data path for split '{self.split}' not found or is not a valid directory: {data_path}\")\n        \n        search_path = os.path.join(data_path, \"**\", \"traj_data.json\")\n        game_files = glob.glob(search_path, recursive=True)\n\n        if self.task_types:\n            def _extract_mapped_task_type(path: str) -> Optional[str]:\n                try:\n                    parts = os.path.normpath(path).split(os.sep)\n                    task_dir = parts[-3].lower()\n                except Exception:\n                    return None\n\n                for k, v in prefixes.items():\n                    if task_dir.startswith(k):\n                        return v\n                return task_dir\n\n            filtered: List[str] = []\n            for gf in game_files:\n                mapped = _extract_mapped_task_type(gf)\n                if mapped and mapped in self.task_types:\n                    filtered.append(gf)\n\n            game_files = filtered\n\n        if self.logger and not game_files:\n            self.logger.warning(\n                f\"No game files found for split '{self.split}' at path '{search_path}'\"\n                + (f\" after applying task_type filter {self.task_types}\" if self.task_types else \"\")\n            )\n\n        filtered_with_pddl: List[str] = []\n        missing_pddl_count = 0\n        for traj_path in game_files:\n            pddl_path = traj_path.replace(\"traj_data.json\", \"game.tw-pddl\")\n            if os.path.exists(pddl_path):\n                filtered_with_pddl.append(traj_path)\n            else:\n                missing_pddl_count += 1\n\n        game_files = filtered_with_pddl\n\n        if self.logger and missing_pddl_count:\n            self.logger.info(\n                f\"Skipped {missing_pddl_count} game(s) without corresponding game.tw-pddl files.\"\n            )\n\n        random.seed(seed)\n        random.shuffle(game_files) \n\n        self.game_files = game_files\n        AlfworldEnv._cached_game_files[cache_key] = game_files\n        return self.game_files\n\n    def _normalize_split_for_alfworld(self, split: str) -> str:\n        \"\"\"Map user/config split names to ALFWorld's expected names.\"\"\"\n        s = (split or \"train\").lower()\n        if s in {\"valid\", \"valid_seen\", \"eval_id\", \"eval_in_distribution\"}:\n            return \"eval_in_distribution\"\n        if s in {\"test\", \"valid_unseen\", \"eval_ood\", \"eval_out_of_distribution\"}:\n            return \"eval_out_of_distribution\"\n        return \"train\"\n\n    def _initialize(self) -> None:\n        \"\"\"Initialize the ALFWorld environment, optionally targeting a specific game file.\"\"\"\n        normalized_split = self._normalize_split_for_alfworld(self.split)\n\n        if self.logger:\n            self.logger.info(f\"Initializing ALFWorld environment with split: {normalized_split}\")\n            if self.specific_game_file:\n                self.logger.info(f\"Target game file: {self.specific_game_file}\")\n\n        with open(self.base_config_path) as reader:\n            config = yaml.safe_load(reader)\n\n        env_type = config[\"env\"][\"type\"]\n        env_class = get_environment(env_type)\n\n        if self.specific_game_file:\n            self._configure_for_specific_game(config, self.specific_game_file)\n            # Provide external game files list to avoid ALFWorld scanning on init\n            pddl_game_file = self.specific_game_file.replace(\"traj_data.json\", \"game.tw-pddl\")\n            config.setdefault(\"env\", {})\n            config[\"env\"][\"external_game_files\"] = [pddl_game_file]\n        elif self.task_types:\n            # Precompute filtered PDDL files and pass to ALFWorld to skip scanning\n            filtered_traj_files = self._get_game_files()\n            filtered_pddl_files = [f.replace(\"traj_data.json\", \"game.tw-pddl\") for f in filtered_traj_files]\n            config.setdefault(\"env\", {})\n            config[\"env\"][\"external_game_files\"] = filtered_pddl_files\n\n        with open(os.devnull, \"w\") as devnull, contextlib.redirect_stdout(\n            devnull\n        ), contextlib.redirect_stderr(devnull):\n            alfworld_env = env_class(config, train_eval=normalized_split)\n\n            # Backward-compatibility: explicitly set game_files on the ALFWorld env instance\n            # so that even if the package doesn't support external_game_files, we still avoid rescans\n            if self.specific_game_file:\n                pddl_game_file = self.specific_game_file.replace(\"traj_data.json\", \"game.tw-pddl\")\n                alfworld_env.game_files = [pddl_game_file]\n                alfworld_env.num_games = 1\n            elif self.task_types:\n                filtered_traj_files = self._get_game_files()\n                filtered_pddl_files = [f.replace(\"traj_data.json\", \"game.tw-pddl\") for f in filtered_traj_files]\n                alfworld_env.game_files = filtered_pddl_files\n                alfworld_env.num_games = len(filtered_pddl_files)\n                if self.logger:\n                    self.logger.info(\n                        f\"Task-type filter active. Loaded {len(filtered_pddl_files)} games for types {self.task_types}.\"\n                    )\n\n            self.env = alfworld_env.init_env(batch_size=1)\n\n        if self.logger:\n            self.logger.info(\"ALFWorld environment initialized successfully\")\n\n    def _configure_for_specific_game(self, config: Dict[str, Any], game_file: str) -> None:\n        \"\"\"Modify the config to load a specific game file.\"\"\"\n        if not os.path.exists(game_file):\n            raise FileNotFoundError(f\"Specific game file not found: {game_file}\")\n\n        if self.split == \"eval_out_of_distribution\":\n            data_path_key = \"eval_ood_data_path\"\n        elif self.split == \"eval_in_distribution\":\n            data_path_key = \"eval_id_data_path\"\n        else:\n            data_path_key = \"data_path\"\n\n        split_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(game_file)))\n        config[\"dataset\"][data_path_key] = split_root_dir\n\n        num_games_key = data_path_key.replace(\"data_path\", \"num_games\").replace(\"eval_id\", \"num_eval\").replace(\"eval_ood\", \"num_eval\")\n        if num_games_key in config[\"dataset\"]:\n            del config[\"dataset\"][num_games_key]\n\n        pddl_game_file = game_file.replace(\"traj_data.json\", \"game.tw-pddl\")\n        if not os.path.exists(pddl_game_file):\n            if self.logger:\n                self.logger.warning(\n                    f\"PDDL file not found for {game_file}. Enabling regen_game_files so it will be generated.\"\n                )\n\n            if \"env\" not in config:\n                config[\"env\"] = {}\n\n            config[\"env\"][\"regen_game_files\"] = True\n\n    def reset(self, running_config: dict, id: Optional[str] = None) -> dict:\n        \"\"\"Reset environment to initial state and return the first observation.\"\"\"\n        if self.logger:\n            self.logger.info(\"Resetting ALFWorld environment\")\n        \n        seed = running_config.get(\"seed\", 42) if running_config else 42\n        task_type_filter = running_config.get(\"task_type\", None) if running_config else None\n        self.split = running_config.get(\"split\", \"train\") if running_config else \"train\"\n        self.id = id\n        id_int: Optional[int] = None\n\n        if id is not None:\n            try:\n                id_int = int(id)\n            except ValueError:\n                raise ValueError(f\"Task ID '{id}' is not a valid integer.\")\n            if self.game_files is None:\n                self.game_files = self._get_game_files(seed)\n            if not 0 <= id_int < len(self.game_files):\n                raise ValueError(\n                    f\"Task ID {id_int} is out of valid range (0-{len(self.game_files) - 1}).\"\n                )\n            if task_type_filter:\n                game_files = []\n                for game_file in self.game_files:\n                    task_type = game_file.split(\"/\")[-3]\n                    if task_type in task_type_filter:\n                        game_files.append(game_file)\n                self.game_files = game_files\n\n            self.specific_game_file = self.game_files[id_int]\n            self.task_type = self.specific_game_file.split(\"/\")[-3]\n            for k, v in prefixes.items():\n                if self.task_type.startswith(k):\n                    self.task_type = v\n                    break\n            \n            if self.logger:\n                self.logger.info(f\"Task type: {self.task_type}\")\n            self.env = None  # Force re-initialization for the specific game\n            if self.logger:\n                self.logger.info(f\"Set to run specific game file for ID {id}: {self.specific_game_file}\")\n\n        if self.env is None:\n            self._initialize()\n\n        if self.env is None:\n            raise ValueError(\"Environment could not be initialized.\")\n\n        ob_raw, info_raw = self.env.reset()\n        # Reset step counter and status flags on env reset\n        self._step_count = 0\n        self._done = False\n        self._success = False\n\n        # Extract game name for logging/debugging\n        self.game_name = \"unknown_game\"\n        if \"extra.gamefile\" in info_raw and info_raw[\"extra.gamefile\"]:\n            try:\n                self.game_name = \"/\".join(info_raw[\"extra.gamefile\"][0].split(\"/\")[-3:-1])\n            except Exception as e:\n                if self.logger:\n                    self.logger.warning(f\"Could not parse game name from info: {e}\")\n\n        # Process observation for the agent\n        obs = \"\\n\".join(ob_raw[0].split(\"\\n\\n\")[1:])\n        # self.logger.info(f\"[Observation ENV] {obs}\")\n        # Return unified reset format\n        return {\"observations\": [obs], \"task_type\": self.task_type, \"env_name\": self.env_name, \"env\": self}\n\n    def set_max_steps(self, max_steps: int) -> None:\n        self.max_steps = max_steps\n\n    async def _run(self, single_action: str) -> str:\n        \"\"\"Execute a *single* action and return the processed observation string.\"\"\"\n        # self.logger.info(f\"Running action: {single_action}\")\n\n        if not single_action:\n            return \"\"\n\n        if single_action.strip() == \"[FINISH]\":\n            self._done = True\n            return \"Episode terminated by agent.\"\n\n        if self._done:\n            return \"The environment has already terminated.\"\n\n        self._step_count += 1\n        if self.max_steps is not None and self._step_count > self.max_steps:\n            self._done = True\n            raise StepLimitError(f\"Step limit of {self.max_steps} exceeded.\")\n\n        pattern = r\"^(put\\s+\\S+(?:\\s+\\S+)*\\s+)(in|on)(\\s+\\S+(?:\\s+\\S+)*)$\"\n        match = re.match(pattern, single_action.strip())\n        if match:\n            single_action = f\"{match.group(1)}in/on{match.group(3)}\"\n\n        def _process_ob(ob: str) -> str:\n            if ob.startswith('You arrive at loc '):\n                ob = ob[ob.find('. ')+2:]\n            return ob\n\n        try:\n            obs_raw, _, done, info = self.env.step([single_action])\n            processed_obs = _process_ob(obs_raw[0])\n            self._done = bool(done[0])\n            self._success = \"won\" in info and bool(info[\"won\"][0])\n            return processed_obs\n        except Exception as e:\n            if self.logger:\n                self.logger.error(f\"Error executing command '{single_action}': {e}\")\n            self._done = True\n            self._success = False\n            return f\"Error: {e}\"\n        \n    def report(self) -> dict:\n        return {\n            \"success\": self._success,\n            \"steps\": self._step_count,\n            \"task_type\": self.task_type,\n            \"reward\": int(self._success)\n        }\n\n    async def close(self) -> None:\n        \"\"\"Close the ALFWorld environment and clean up resources.\"\"\"\n        if self.logger:\n            self.logger.info(\"Closing ALFWorld environment\")\n        \n        try:\n            # Clean up the ALFWorld environment if it exists\n            if hasattr(self, 'env') and self.env is not None:\n                # ALFWorld environment cleanup\n                self.env = None\n                \n            # Reset state variables\n            self._step_count = 0\n            self._done = False\n            self._success = False\n            self.game_files = None\n            self.game_name = \"unknown_game\"\n            \n            if self.logger:\n                self.logger.info(\"ALFWorld environment closed successfully\")\n                \n        except Exception as e:\n            if self.logger:\n                self.logger.error(f\"Error closing ALFWorld environment: {e}\")\n            raise"
  },
  {
    "path": "envs/sciworld/base_config.yaml",
    "content": "data_root_dir: \"envs/sciworld/data\""
  },
  {
    "path": "envs/sciworld/data/max_steps.json",
    "content": "{\n    \"task-1-boil\": 100,\n    \"task-1-change-the-state-of-matter-of\": 80,\n    \"task-1-freeze\": 80,\n    \"task-1-melt\": 80,\n    \"task-10-measure-melting-point-(known-substance)\": 120,\n    \"task-10-use-thermometer\": 30,\n    \"task-2-power-component\": 20,\n    \"task-2-power-component-(renewable-vs-nonrenewable-energy)\": 30,\n    \"task-2a-test-conductivity\": 30,\n    \"task-2a-test-conductivity-of-unknown-substances\": 30,\n    \"task-3-find-animal\": 15,\n    \"task-3-find-living-thing\": 15,\n    \"task-3-find-non-living-thing\": 15,\n    \"task-3-find-plant\": 15,\n    \"task-4-grow-fruit\": 60,\n    \"task-4-grow-plant\": 30,\n    \"task-5-chemistry-mix\": 60,\n    \"task-5-chemistry-mix-paint-(secondary-color)\": 15,\n    \"task-5-chemistry-mix-paint-(tertiary-color)\": 30,\n    \"task-6-lifespan-(longest-lived)\": 10,\n    \"task-6-lifespan-(longest-lived-then-shortest-lived)\": 12,\n    \"task-6-lifespan-(shortest-lived)\": 10,\n    \"task-7-identify-life-stages-1\": 30,\n    \"task-7-identify-life-stages-2\": 30\n}"
  },
  {
    "path": "envs/sciworld/data/taskname2id.json",
    "content": "{\n    \"task-1-boil\": 0,\n    \"task-1-change-the-state-of-matter-of\": 1,\n    \"task-1-freeze\": 2,\n    \"task-1-melt\": 3,\n    \"task-10-measure-melting-point-(known-substance)\": 4,\n    \"task-10-use-thermometer\": 6,\n    \"task-2-power-component\": 7,\n    \"task-2-power-component-(renewable-vs-nonrenewable-energy)\": 8,\n    \"task-2a-test-conductivity\": 9,\n    \"task-2a-test-conductivity-of-unknown-substances\": 10,\n    \"task-3-find-animal\": 11,\n    \"task-3-find-living-thing\": 12,\n    \"task-3-find-non-living-thing\": 13,\n    \"task-3-find-plant\": 14,\n    \"task-4-grow-fruit\": 15,\n    \"task-4-grow-plant\": 16,\n    \"task-5-chemistry-mix\": 17,\n    \"task-5-chemistry-mix-paint-(secondary-color)\": 18,\n    \"task-5-chemistry-mix-paint-(tertiary-color)\": 19,\n    \"task-6-lifespan-(longest-lived)\": 20,\n    \"task-6-lifespan-(longest-lived-then-shortest-lived)\": 21,\n    \"task-6-lifespan-(shortest-lived)\": 22,\n    \"task-7-identify-life-stages-1\": 23,\n    \"task-7-identify-life-stages-2\": 24,\n    \"task-8-inclined-plane-determine-angle\": 25,\n    \"task-8-inclined-plane-friction-(named-surfaces)\": 26,\n    \"task-8-inclined-plane-friction-(unnamed-surfaces)\": 27,\n    \"task-9-mendellian-genetics-(known-plant)\": 28,\n    \"task-9-mendellian-genetics-(unknown-plant)\": 29\n}"
  },
  {
    "path": "envs/sciworld/data/test_indices.json",
    "content": "[\n    [\n        \"task-1-boil\",\n        21\n    ],\n    [\n        \"task-1-boil\",\n        22\n    ],\n    [\n        \"task-1-boil\",\n        23\n    ],\n    [\n        \"task-1-boil\",\n        24\n    ],\n    [\n        \"task-1-boil\",\n        25\n    ],\n    [\n        \"task-1-boil\",\n        26\n    ],\n    [\n        \"task-1-boil\",\n        27\n    ],\n    [\n        \"task-1-boil\",\n        28\n    ],\n    [\n        \"task-1-boil\",\n        29\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        21\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        22\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        23\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        24\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        25\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        26\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        27\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        28\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        29\n    ],\n    [\n        \"task-1-freeze\",\n        21\n    ],\n    [\n        \"task-1-freeze\",\n        22\n    ],\n    [\n        \"task-1-freeze\",\n        23\n    ],\n    [\n        \"task-1-freeze\",\n        24\n    ],\n    [\n        \"task-1-freeze\",\n        25\n    ],\n    [\n        \"task-1-freeze\",\n        26\n    ],\n    [\n        \"task-1-freeze\",\n        27\n    ],\n    [\n        \"task-1-freeze\",\n        28\n    ],\n    [\n        \"task-1-freeze\",\n        29\n    ],\n    [\n        \"task-1-melt\",\n        21\n    ],\n    [\n        \"task-1-melt\",\n        22\n    ],\n    [\n        \"task-1-melt\",\n        23\n    ],\n    [\n        \"task-1-melt\",\n        24\n    ],\n    [\n        \"task-1-melt\",\n        25\n    ],\n    [\n        \"task-1-melt\",\n        26\n    ],\n    [\n        \"task-1-melt\",\n        27\n    ],\n    [\n        \"task-1-melt\",\n        28\n    ],\n    [\n        \"task-1-melt\",\n        29\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        327\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        328\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        329\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        330\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        331\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        332\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        333\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        334\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        335\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        336\n    ],\n    [\n        \"task-10-use-thermometer\",\n        405\n    ],\n    [\n        \"task-10-use-thermometer\",\n        406\n    ],\n    [\n        \"task-10-use-thermometer\",\n        407\n    ],\n    [\n        \"task-10-use-thermometer\",\n        408\n    ],\n    [\n        \"task-10-use-thermometer\",\n        409\n    ],\n    [\n        \"task-10-use-thermometer\",\n        410\n    ],\n    [\n        \"task-10-use-thermometer\",\n        411\n    ],\n    [\n        \"task-10-use-thermometer\",\n        412\n    ],\n    [\n        \"task-10-use-thermometer\",\n        413\n    ],\n    [\n        \"task-10-use-thermometer\",\n        414\n    ],\n    [\n        \"task-2-power-component\",\n        15\n    ],\n    [\n        \"task-2-power-component\",\n        16\n    ],\n    [\n        \"task-2-power-component\",\n        17\n    ],\n    [\n        \"task-2-power-component\",\n        18\n    ],\n    [\n        \"task-2-power-component\",\n        19\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        15\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        16\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        17\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        18\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        19\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        675\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        676\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        677\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        678\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        679\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        680\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        681\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        682\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        683\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        684\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        450\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        451\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        452\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        453\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        454\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        455\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        456\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        457\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        458\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        459\n    ],\n    [\n        \"task-3-find-animal\",\n        225\n    ],\n    [\n        \"task-3-find-animal\",\n        226\n    ],\n    [\n        \"task-3-find-animal\",\n        227\n    ],\n    [\n        \"task-3-find-animal\",\n        228\n    ],\n    [\n        \"task-3-find-animal\",\n        229\n    ],\n    [\n        \"task-3-find-animal\",\n        230\n    ],\n    [\n        \"task-3-find-animal\",\n        231\n    ],\n    [\n        \"task-3-find-animal\",\n        232\n    ],\n    [\n        \"task-3-find-animal\",\n        233\n    ],\n    [\n        \"task-3-find-animal\",\n        234\n    ],\n    [\n        \"task-3-find-living-thing\",\n        225\n    ],\n    [\n        \"task-3-find-living-thing\",\n        226\n    ],\n    [\n        \"task-3-find-living-thing\",\n        227\n    ],\n    [\n        \"task-3-find-living-thing\",\n        228\n    ],\n    [\n        \"task-3-find-living-thing\",\n        229\n    ],\n    [\n        \"task-3-find-living-thing\",\n        230\n    ],\n    [\n        \"task-3-find-living-thing\",\n        231\n    ],\n    [\n        \"task-3-find-living-thing\",\n        232\n    ],\n    [\n        \"task-3-find-living-thing\",\n        233\n    ],\n    [\n        \"task-3-find-living-thing\",\n        234\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        225\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        226\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        227\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        228\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        229\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        230\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        231\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        232\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        233\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        234\n    ],\n    [\n        \"task-3-find-plant\",\n        225\n    ],\n    [\n        \"task-3-find-plant\",\n        226\n    ],\n    [\n        \"task-3-find-plant\",\n        227\n    ],\n    [\n        \"task-3-find-plant\",\n        228\n    ],\n    [\n        \"task-3-find-plant\",\n        229\n    ],\n    [\n        \"task-3-find-plant\",\n        230\n    ],\n    [\n        \"task-3-find-plant\",\n        231\n    ],\n    [\n        \"task-3-find-plant\",\n        232\n    ],\n    [\n        \"task-3-find-plant\",\n        233\n    ],\n    [\n        \"task-3-find-plant\",\n        234\n    ],\n    [\n        \"task-4-grow-fruit\",\n        93\n    ],\n    [\n        \"task-4-grow-fruit\",\n        94\n    ],\n    [\n        \"task-4-grow-fruit\",\n        95\n    ],\n    [\n        \"task-4-grow-fruit\",\n        96\n    ],\n    [\n        \"task-4-grow-fruit\",\n        97\n    ],\n    [\n        \"task-4-grow-fruit\",\n        98\n    ],\n    [\n        \"task-4-grow-fruit\",\n        99\n    ],\n    [\n        \"task-4-grow-fruit\",\n        100\n    ],\n    [\n        \"task-4-grow-fruit\",\n        101\n    ],\n    [\n        \"task-4-grow-fruit\",\n        102\n    ],\n    [\n        \"task-4-grow-plant\",\n        93\n    ],\n    [\n        \"task-4-grow-plant\",\n        94\n    ],\n    [\n        \"task-4-grow-plant\",\n        95\n    ],\n    [\n        \"task-4-grow-plant\",\n        96\n    ],\n    [\n        \"task-4-grow-plant\",\n        97\n    ],\n    [\n        \"task-4-grow-plant\",\n        98\n    ],\n    [\n        \"task-4-grow-plant\",\n        99\n    ],\n    [\n        \"task-4-grow-plant\",\n        100\n    ],\n    [\n        \"task-4-grow-plant\",\n        101\n    ],\n    [\n        \"task-4-grow-plant\",\n        102\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        24\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        25\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        26\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        27\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        28\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        29\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        30\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        31\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        27\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        28\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        29\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        30\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        31\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        32\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        33\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        34\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        35\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        27\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        28\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        29\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        30\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        31\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        32\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        33\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        34\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        35\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        93\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        94\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        95\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        96\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        97\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        98\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        99\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        100\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        101\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        102\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        93\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        94\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        95\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        96\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        97\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        98\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        99\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        100\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        101\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        102\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        93\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        94\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        95\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        96\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        97\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        98\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        99\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        100\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        101\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        102\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        9\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        10\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        11\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        12\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        13\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        6\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        7\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        8\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        9\n    ]\n]"
  },
  {
    "path": "envs/sciworld/data/train_indices.json",
    "content": "[\n    [\n        \"task-2a-test-conductivity\",\n        412\n    ],\n    [\n        \"task-3-find-animal\",\n        0\n    ],\n    [\n        \"task-4-grow-fruit\",\n        53\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        52\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        262\n    ],\n    [\n        \"task-10-use-thermometer\",\n        45\n    ],\n    [\n        \"task-10-use-thermometer\",\n        141\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        99\n    ],\n    [\n        \"task-10-use-thermometer\",\n        172\n    ],\n    [\n        \"task-3-find-plant\",\n        70\n    ],\n    [\n        \"task-3-find-living-thing\",\n        147\n    ],\n    [\n        \"task-10-use-thermometer\",\n        191\n    ],\n    [\n        \"task-4-grow-fruit\",\n        11\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        274\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        8\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        59\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        20\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        167\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        126\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        84\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        195\n    ],\n    [\n        \"task-3-find-living-thing\",\n        21\n    ],\n    [\n        \"task-4-grow-fruit\",\n        23\n    ],\n    [\n        \"task-4-grow-fruit\",\n        20\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        397\n    ],\n    [\n        \"task-3-find-plant\",\n        131\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        394\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        15\n    ],\n    [\n        \"task-3-find-living-thing\",\n        89\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        51\n    ],\n    [\n        \"task-3-find-plant\",\n        30\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        18\n    ],\n    [\n        \"task-4-grow-plant\",\n        29\n    ],\n    [\n        \"task-3-find-plant\",\n        56\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        15\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        197\n    ],\n    [\n        \"task-3-find-living-thing\",\n        35\n    ],\n    [\n        \"task-3-find-living-thing\",\n        43\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        23\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        40\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        390\n    ],\n    [\n        \"task-3-find-living-thing\",\n        38\n    ],\n    [\n        \"task-4-grow-plant\",\n        41\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        21\n    ],\n    [\n        \"task-3-find-plant\",\n        91\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        188\n    ],\n    [\n        \"task-3-find-plant\",\n        74\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        5\n    ],\n    [\n        \"task-10-use-thermometer\",\n        267\n    ],\n    [\n        \"task-10-use-thermometer\",\n        100\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        59\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        30\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        99\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        53\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        0\n    ],\n    [\n        \"task-3-find-living-thing\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        10\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        336\n    ],\n    [\n        \"task-3-find-animal\",\n        54\n    ],\n    [\n        \"task-3-find-plant\",\n        85\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        12\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        204\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        15\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        63\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        247\n    ],\n    [\n        \"task-4-grow-plant\",\n        33\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        1\n    ],\n    [\n        \"task-3-find-plant\",\n        71\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        79\n    ],\n    [\n        \"task-3-find-animal\",\n        22\n    ],\n    [\n        \"task-3-find-living-thing\",\n        108\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        58\n    ],\n    [\n        \"task-3-find-plant\",\n        47\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        300\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        59\n    ],\n    [\n        \"task-10-use-thermometer\",\n        87\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        16\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        339\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        38\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        45\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        47\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        3\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        22\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        78\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        61\n    ],\n    [\n        \"task-4-grow-plant\",\n        1\n    ],\n    [\n        \"task-10-use-thermometer\",\n        214\n    ],\n    [\n        \"task-4-grow-plant\",\n        5\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        18\n    ],\n    [\n        \"task-3-find-living-thing\",\n        121\n    ],\n    [\n        \"task-1-boil\",\n        4\n    ],\n    [\n        \"task-3-find-living-thing\",\n        140\n    ],\n    [\n        \"task-10-use-thermometer\",\n        170\n    ],\n    [\n        \"task-3-find-plant\",\n        49\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        228\n    ],\n    [\n        \"task-4-grow-plant\",\n        51\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        184\n    ],\n    [\n        \"task-10-use-thermometer\",\n        133\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        96\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        311\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        401\n    ],\n    [\n        \"task-10-use-thermometer\",\n        107\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        94\n    ],\n    [\n        \"task-10-use-thermometer\",\n        72\n    ],\n    [\n        \"task-10-use-thermometer\",\n        219\n    ],\n    [\n        \"task-1-boil\",\n        11\n    ],\n    [\n        \"task-3-find-plant\",\n        94\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        145\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        154\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        89\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        130\n    ],\n    [\n        \"task-1-melt\",\n        2\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        121\n    ],\n    [\n        \"task-3-find-plant\",\n        135\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        114\n    ],\n    [\n        \"task-3-find-plant\",\n        55\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        417\n    ],\n    [\n        \"task-10-use-thermometer\",\n        209\n    ],\n    [\n        \"task-10-use-thermometer\",\n        82\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        230\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        192\n    ],\n    [\n        \"task-10-use-thermometer\",\n        248\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        145\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        344\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        2\n    ],\n    [\n        \"task-3-find-animal\",\n        56\n    ],\n    [\n        \"task-10-use-thermometer\",\n        210\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        1\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        6\n    ],\n    [\n        \"task-10-use-thermometer\",\n        49\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        30\n    ],\n    [\n        \"task-3-find-living-thing\",\n        94\n    ],\n    [\n        \"task-10-use-thermometer\",\n        198\n    ],\n    [\n        \"task-10-use-thermometer\",\n        61\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        27\n    ],\n    [\n        \"task-10-use-thermometer\",\n        202\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        330\n    ],\n    [\n        \"task-10-use-thermometer\",\n        105\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        238\n    ],\n    [\n        \"task-10-use-thermometer\",\n        256\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        420\n    ],\n    [\n        \"task-4-grow-plant\",\n        60\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        49\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        39\n    ],\n    [\n        \"task-10-use-thermometer\",\n        156\n    ],\n    [\n        \"task-3-find-animal\",\n        26\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        134\n    ],\n    [\n        \"task-4-grow-plant\",\n        38\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        109\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        29\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        95\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        1\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        369\n    ],\n    [\n        \"task-3-find-plant\",\n        121\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        0\n    ],\n    [\n        \"task-10-use-thermometer\",\n        276\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        54\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        147\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        47\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        101\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        4\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        92\n    ],\n    [\n        \"task-4-grow-fruit\",\n        52\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        62\n    ],\n    [\n        \"task-3-find-plant\",\n        89\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        42\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        315\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        240\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        36\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        7\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        22\n    ],\n    [\n        \"task-3-find-plant\",\n        103\n    ],\n    [\n        \"task-10-use-thermometer\",\n        221\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        139\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        12\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        163\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        49\n    ],\n    [\n        \"task-10-use-thermometer\",\n        171\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        64\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        531\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        9\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        394\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        154\n    ],\n    [\n        \"task-2-power-component\",\n        1\n    ],\n    [\n        \"task-3-find-plant\",\n        44\n    ],\n    [\n        \"task-3-find-living-thing\",\n        149\n    ],\n    [\n        \"task-10-use-thermometer\",\n        132\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        247\n    ],\n    [\n        \"task-10-use-thermometer\",\n        282\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        143\n    ],\n    [\n        \"task-3-find-animal\",\n        4\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        673\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        525\n    ],\n    [\n        \"task-1-freeze\",\n        9\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        14\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        332\n    ],\n    [\n        \"task-1-freeze\",\n        5\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        186\n    ],\n    [\n        \"task-1-melt\",\n        10\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        489\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        345\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        348\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        2\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        118\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        488\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        132\n    ],\n    [\n        \"task-3-find-living-thing\",\n        128\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        427\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        323\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        13\n    ],\n    [\n        \"task-3-find-animal\",\n        29\n    ],\n    [\n        \"task-3-find-animal\",\n        34\n    ],\n    [\n        \"task-10-use-thermometer\",\n        77\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        665\n    ],\n    [\n        \"task-10-use-thermometer\",\n        366\n    ],\n    [\n        \"task-10-use-thermometer\",\n        224\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        39\n    ],\n    [\n        \"task-10-use-thermometer\",\n        192\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        65\n    ],\n    [\n        \"task-10-use-thermometer\",\n        158\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        110\n    ],\n    [\n        \"task-3-find-living-thing\",\n        86\n    ],\n    [\n        \"task-10-use-thermometer\",\n        326\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        61\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        10\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        47\n    ],\n    [\n        \"task-1-boil\",\n        13\n    ],\n    [\n        \"task-3-find-animal\",\n        69\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        309\n    ],\n    [\n        \"task-3-find-animal\",\n        101\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        235\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        159\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        105\n    ],\n    [\n        \"task-4-grow-plant\",\n        26\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        158\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        57\n    ],\n    [\n        \"task-3-find-living-thing\",\n        135\n    ],\n    [\n        \"task-3-find-plant\",\n        68\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        648\n    ],\n    [\n        \"task-3-find-animal\",\n        10\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        537\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        124\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        81\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        81\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        28\n    ],\n    [\n        \"task-3-find-living-thing\",\n        124\n    ],\n    [\n        \"task-4-grow-fruit\",\n        56\n    ],\n    [\n        \"task-3-find-living-thing\",\n        103\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        38\n    ],\n    [\n        \"task-10-use-thermometer\",\n        343\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        461\n    ],\n    [\n        \"task-3-find-animal\",\n        99\n    ],\n    [\n        \"task-3-find-animal\",\n        128\n    ],\n    [\n        \"task-3-find-plant\",\n        37\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        121\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        301\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        61\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        36\n    ],\n    [\n        \"task-3-find-living-thing\",\n        41\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        217\n    ],\n    [\n        \"task-10-use-thermometer\",\n        59\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        116\n    ],\n    [\n        \"task-3-find-plant\",\n        24\n    ],\n    [\n        \"task-10-use-thermometer\",\n        370\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        429\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        265\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        610\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        581\n    ],\n    [\n        \"task-10-use-thermometer\",\n        296\n    ],\n    [\n        \"task-4-grow-plant\",\n        11\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        51\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        329\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        125\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        273\n    ],\n    [\n        \"task-10-use-thermometer\",\n        228\n    ],\n    [\n        \"task-3-find-animal\",\n        9\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        22\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        321\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        362\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        66\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        9\n    ],\n    [\n        \"task-10-use-thermometer\",\n        404\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        254\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        42\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        396\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        72\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        40\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        290\n    ],\n    [\n        \"task-3-find-plant\",\n        0\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        152\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        560\n    ],\n    [\n        \"task-10-use-thermometer\",\n        92\n    ],\n    [\n        \"task-1-melt\",\n        12\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        590\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        14\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        460\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        215\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        114\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        72\n    ],\n    [\n        \"task-3-find-animal\",\n        148\n    ],\n    [\n        \"task-4-grow-plant\",\n        7\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        11\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        85\n    ],\n    [\n        \"task-3-find-plant\",\n        19\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        294\n    ],\n    [\n        \"task-3-find-plant\",\n        129\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        294\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        41\n    ],\n    [\n        \"task-10-use-thermometer\",\n        389\n    ],\n    [\n        \"task-3-find-plant\",\n        40\n    ],\n    [\n        \"task-10-use-thermometer\",\n        42\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        34\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        406\n    ],\n    [\n        \"task-10-use-thermometer\",\n        86\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        360\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        587\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        386\n    ],\n    [\n        \"task-10-use-thermometer\",\n        330\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        319\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        253\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        2\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        573\n    ],\n    [\n        \"task-3-find-plant\",\n        98\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        151\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        35\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        407\n    ],\n    [\n        \"task-10-use-thermometer\",\n        301\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        57\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        148\n    ],\n    [\n        \"task-10-use-thermometer\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        8\n    ],\n    [\n        \"task-10-use-thermometer\",\n        138\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        312\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        626\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        258\n    ],\n    [\n        \"task-3-find-living-thing\",\n        51\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        40\n    ],\n    [\n        \"task-10-use-thermometer\",\n        353\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        465\n    ],\n    [\n        \"task-3-find-animal\",\n        130\n    ],\n    [\n        \"task-10-use-thermometer\",\n        136\n    ],\n    [\n        \"task-3-find-animal\",\n        113\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        36\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        83\n    ],\n    [\n        \"task-10-use-thermometer\",\n        193\n    ],\n    [\n        \"task-3-find-animal\",\n        223\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        67\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        115\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        431\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        49\n    ],\n    [\n        \"task-3-find-animal\",\n        143\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        188\n    ],\n    [\n        \"task-3-find-living-thing\",\n        15\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        541\n    ],\n    [\n        \"task-3-find-plant\",\n        62\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        102\n    ],\n    [\n        \"task-3-find-plant\",\n        60\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        293\n    ],\n    [\n        \"task-3-find-living-thing\",\n        30\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        349\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        205\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        211\n    ],\n    [\n        \"task-10-use-thermometer\",\n        108\n    ],\n    [\n        \"task-10-use-thermometer\",\n        237\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        38\n    ],\n    [\n        \"task-3-find-plant\",\n        65\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        250\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        8\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        172\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        425\n    ],\n    [\n        \"task-10-use-thermometer\",\n        381\n    ],\n    [\n        \"task-10-use-thermometer\",\n        187\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        303\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        44\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        1\n    ],\n    [\n        \"task-3-find-animal\",\n        76\n    ],\n    [\n        \"task-3-find-animal\",\n        17\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        22\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        500\n    ],\n    [\n        \"task-3-find-living-thing\",\n        78\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        188\n    ],\n    [\n        \"task-3-find-animal\",\n        45\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        397\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        101\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        105\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        324\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        459\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        456\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        133\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        181\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        135\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        306\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        1\n    ],\n    [\n        \"task-3-find-living-thing\",\n        116\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        230\n    ],\n    [\n        \"task-10-use-thermometer\",\n        399\n    ],\n    [\n        \"task-3-find-plant\",\n        27\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        223\n    ],\n    [\n        \"task-10-use-thermometer\",\n        361\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        201\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        6\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        15\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        248\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        34\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        521\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        254\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        4\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        178\n    ],\n    [\n        \"task-3-find-living-thing\",\n        129\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        234\n    ],\n    [\n        \"task-10-use-thermometer\",\n        95\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        618\n    ],\n    [\n        \"task-3-find-plant\",\n        146\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        128\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        44\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        98\n    ],\n    [\n        \"task-4-grow-fruit\",\n        58\n    ],\n    [\n        \"task-10-use-thermometer\",\n        254\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        202\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        423\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        328\n    ],\n    [\n        \"task-3-find-animal\",\n        83\n    ],\n    [\n        \"task-3-find-living-thing\",\n        138\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        47\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        72\n    ],\n    [\n        \"task-3-find-animal\",\n        97\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        385\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        189\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        553\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        156\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        142\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        196\n    ],\n    [\n        \"task-10-use-thermometer\",\n        68\n    ],\n    [\n        \"task-10-use-thermometer\",\n        252\n    ],\n    [\n        \"task-3-find-living-thing\",\n        68\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        54\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        7\n    ],\n    [\n        \"task-3-find-living-thing\",\n        24\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        510\n    ],\n    [\n        \"task-4-grow-fruit\",\n        3\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        34\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        176\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        4\n    ],\n    [\n        \"task-4-grow-plant\",\n        48\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        331\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        196\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        13\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        359\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        10\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        50\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        8\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        35\n    ],\n    [\n        \"task-4-grow-plant\",\n        14\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        39\n    ],\n    [\n        \"task-3-find-living-thing\",\n        143\n    ],\n    [\n        \"task-3-find-plant\",\n        52\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        260\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        5\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        371\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        557\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        272\n    ],\n    [\n        \"task-10-use-thermometer\",\n        347\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        61\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        540\n    ],\n    [\n        \"task-3-find-living-thing\",\n        9\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        117\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        236\n    ],\n    [\n        \"task-10-use-thermometer\",\n        235\n    ],\n    [\n        \"task-10-use-thermometer\",\n        265\n    ],\n    [\n        \"task-10-use-thermometer\",\n        398\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        601\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        19\n    ],\n    [\n        \"task-3-find-plant\",\n        194\n    ],\n    [\n        \"task-3-find-animal\",\n        25\n    ],\n    [\n        \"task-3-find-animal\",\n        55\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        81\n    ],\n    [\n        \"task-10-use-thermometer\",\n        197\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        133\n    ],\n    [\n        \"task-3-find-living-thing\",\n        117\n    ],\n    [\n        \"task-3-find-plant\",\n        69\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        295\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        11\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        214\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        52\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        219\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        214\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        377\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        132\n    ],\n    [\n        \"task-3-find-plant\",\n        205\n    ],\n    [\n        \"task-3-find-plant\",\n        122\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        110\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        122\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        217\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        10\n    ],\n    [\n        \"task-10-use-thermometer\",\n        175\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        36\n    ],\n    [\n        \"task-3-find-plant\",\n        95\n    ],\n    [\n        \"task-3-find-animal\",\n        74\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        279\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        415\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        93\n    ],\n    [\n        \"task-10-use-thermometer\",\n        166\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        323\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        264\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        635\n    ],\n    [\n        \"task-3-find-animal\",\n        212\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        243\n    ],\n    [\n        \"task-3-find-plant\",\n        78\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        444\n    ],\n    [\n        \"task-3-find-animal\",\n        147\n    ],\n    [\n        \"task-3-find-plant\",\n        105\n    ],\n    [\n        \"task-3-find-plant\",\n        42\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        123\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        3\n    ],\n    [\n        \"task-3-find-plant\",\n        59\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        225\n    ],\n    [\n        \"task-10-use-thermometer\",\n        335\n    ],\n    [\n        \"task-10-use-thermometer\",\n        149\n    ],\n    [\n        \"task-10-use-thermometer\",\n        279\n    ],\n    [\n        \"task-10-use-thermometer\",\n        98\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        190\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        78\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        20\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        175\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        233\n    ],\n    [\n        \"task-10-use-thermometer\",\n        392\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        319\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        299\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        647\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        120\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        368\n    ],\n    [\n        \"task-3-find-living-thing\",\n        26\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        246\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        210\n    ],\n    [\n        \"task-3-find-animal\",\n        85\n    ],\n    [\n        \"task-3-find-living-thing\",\n        60\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        512\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        231\n    ],\n    [\n        \"task-3-find-animal\",\n        179\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        271\n    ],\n    [\n        \"task-3-find-plant\",\n        64\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        0\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        2\n    ],\n    [\n        \"task-1-boil\",\n        8\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        111\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        162\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        244\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        91\n    ],\n    [\n        \"task-10-use-thermometer\",\n        263\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        178\n    ],\n    [\n        \"task-3-find-animal\",\n        107\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        150\n    ],\n    [\n        \"task-10-use-thermometer\",\n        391\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        47\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        366\n    ],\n    [\n        \"task-3-find-animal\",\n        215\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        38\n    ],\n    [\n        \"task-3-find-animal\",\n        220\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        45\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        255\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        26\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        220\n    ],\n    [\n        \"task-10-use-thermometer\",\n        367\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        2\n    ],\n    [\n        \"task-3-find-animal\",\n        201\n    ],\n    [\n        \"task-3-find-plant\",\n        221\n    ],\n    [\n        \"task-10-use-thermometer\",\n        203\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        124\n    ],\n    [\n        \"task-3-find-plant\",\n        185\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        182\n    ],\n    [\n        \"task-10-use-thermometer\",\n        320\n    ],\n    [\n        \"task-3-find-animal\",\n        19\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        29\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        200\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        441\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        363\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        3\n    ],\n    [\n        \"task-3-find-plant\",\n        1\n    ],\n    [\n        \"task-4-grow-plant\",\n        40\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        98\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        185\n    ],\n    [\n        \"task-10-use-thermometer\",\n        368\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        106\n    ],\n    [\n        \"task-3-find-plant\",\n        187\n    ],\n    [\n        \"task-3-find-plant\",\n        102\n    ],\n    [\n        \"task-3-find-animal\",\n        62\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        195\n    ],\n    [\n        \"task-10-use-thermometer\",\n        188\n    ],\n    [\n        \"task-3-find-plant\",\n        5\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        112\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        50\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        314\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        293\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        177\n    ],\n    [\n        \"task-10-use-thermometer\",\n        91\n    ],\n    [\n        \"task-3-find-animal\",\n        6\n    ],\n    [\n        \"task-10-use-thermometer\",\n        293\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        9\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        51\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        241\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        5\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        85\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        258\n    ],\n    [\n        \"task-3-find-plant\",\n        8\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        150\n    ],\n    [\n        \"task-3-find-living-thing\",\n        58\n    ],\n    [\n        \"task-10-use-thermometer\",\n        401\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        138\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        154\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        16\n    ],\n    [\n        \"task-3-find-plant\",\n        26\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        4\n    ],\n    [\n        \"task-10-use-thermometer\",\n        124\n    ],\n    [\n        \"task-3-find-living-thing\",\n        88\n    ],\n    [\n        \"task-1-melt\",\n        0\n    ],\n    [\n        \"task-4-grow-fruit\",\n        25\n    ],\n    [\n        \"task-10-use-thermometer\",\n        144\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        353\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        0\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        10\n    ],\n    [\n        \"task-1-boil\",\n        7\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        143\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        391\n    ],\n    [\n        \"task-3-find-plant\",\n        211\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        261\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        9\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        141\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        55\n    ],\n    [\n        \"task-10-use-thermometer\",\n        243\n    ],\n    [\n        \"task-4-grow-fruit\",\n        37\n    ],\n    [\n        \"task-4-grow-plant\",\n        25\n    ],\n    [\n        \"task-10-use-thermometer\",\n        206\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        27\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        41\n    ],\n    [\n        \"task-3-find-animal\",\n        68\n    ],\n    [\n        \"task-4-grow-fruit\",\n        6\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        70\n    ],\n    [\n        \"task-10-use-thermometer\",\n        128\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        110\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        422\n    ],\n    [\n        \"task-10-use-thermometer\",\n        121\n    ],\n    [\n        \"task-3-find-living-thing\",\n        48\n    ],\n    [\n        \"task-10-use-thermometer\",\n        225\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        310\n    ],\n    [\n        \"task-1-freeze\",\n        4\n    ],\n    [\n        \"task-10-use-thermometer\",\n        102\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        242\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        198\n    ],\n    [\n        \"task-3-find-animal\",\n        174\n    ],\n    [\n        \"task-10-use-thermometer\",\n        286\n    ],\n    [\n        \"task-10-use-thermometer\",\n        284\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        6\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        181\n    ],\n    [\n        \"task-10-use-thermometer\",\n        155\n    ],\n    [\n        \"task-4-grow-fruit\",\n        55\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        70\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        52\n    ],\n    [\n        \"task-3-find-plant\",\n        164\n    ],\n    [\n        \"task-4-grow-fruit\",\n        2\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        46\n    ],\n    [\n        \"task-3-find-animal\",\n        13\n    ],\n    [\n        \"task-3-find-living-thing\",\n        213\n    ],\n    [\n        \"task-4-grow-plant\",\n        54\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        8\n    ],\n    [\n        \"task-3-find-living-thing\",\n        13\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        147\n    ],\n    [\n        \"task-10-use-thermometer\",\n        169\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        119\n    ],\n    [\n        \"task-10-use-thermometer\",\n        22\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        27\n    ],\n    [\n        \"task-3-find-animal\",\n        53\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        80\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        38\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        162\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        199\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        14\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        398\n    ],\n    [\n        \"task-3-find-plant\",\n        178\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        172\n    ],\n    [\n        \"task-3-find-living-thing\",\n        179\n    ],\n    [\n        \"task-3-find-plant\",\n        72\n    ],\n    [\n        \"task-10-use-thermometer\",\n        81\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        438\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        80\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        153\n    ],\n    [\n        \"task-3-find-animal\",\n        109\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        99\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        88\n    ],\n    [\n        \"task-10-use-thermometer\",\n        337\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        182\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        183\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        0\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        313\n    ],\n    [\n        \"task-10-use-thermometer\",\n        99\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        98\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        289\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        166\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        55\n    ],\n    [\n        \"task-10-use-thermometer\",\n        270\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        15\n    ],\n    [\n        \"task-10-use-thermometer\",\n        190\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        342\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        59\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        74\n    ],\n    [\n        \"task-3-find-living-thing\",\n        206\n    ],\n    [\n        \"task-3-find-living-thing\",\n        125\n    ],\n    [\n        \"task-3-find-animal\",\n        120\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        76\n    ],\n    [\n        \"task-10-use-thermometer\",\n        305\n    ],\n    [\n        \"task-3-find-animal\",\n        84\n    ],\n    [\n        \"task-3-find-animal\",\n        224\n    ],\n    [\n        \"task-10-use-thermometer\",\n        359\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        65\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        4\n    ],\n    [\n        \"task-3-find-animal\",\n        71\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        40\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        58\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        46\n    ],\n    [\n        \"task-3-find-animal\",\n        144\n    ],\n    [\n        \"task-10-use-thermometer\",\n        205\n    ],\n    [\n        \"task-10-use-thermometer\",\n        90\n    ],\n    [\n        \"task-10-use-thermometer\",\n        89\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        146\n    ],\n    [\n        \"task-10-use-thermometer\",\n        216\n    ],\n    [\n        \"task-3-find-animal\",\n        1\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        113\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        64\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        92\n    ],\n    [\n        \"task-3-find-living-thing\",\n        114\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        74\n    ],\n    [\n        \"task-3-find-animal\",\n        156\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        135\n    ],\n    [\n        \"task-3-find-living-thing\",\n        90\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        206\n    ],\n    [\n        \"task-3-find-living-thing\",\n        153\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        330\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        23\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        2\n    ],\n    [\n        \"task-10-use-thermometer\",\n        231\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        302\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        16\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        8\n    ],\n    [\n        \"task-10-use-thermometer\",\n        311\n    ],\n    [\n        \"task-10-use-thermometer\",\n        142\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        30\n    ],\n    [\n        \"task-10-use-thermometer\",\n        317\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        28\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        171\n    ],\n    [\n        \"task-3-find-living-thing\",\n        131\n    ],\n    [\n        \"task-2-power-component\",\n        3\n    ],\n    [\n        \"task-3-find-animal\",\n        108\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        25\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        62\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        216\n    ],\n    [\n        \"task-10-use-thermometer\",\n        97\n    ],\n    [\n        \"task-3-find-living-thing\",\n        74\n    ],\n    [\n        \"task-3-find-living-thing\",\n        172\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        120\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        134\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        1\n    ],\n    [\n        \"task-3-find-plant\",\n        155\n    ],\n    [\n        \"task-4-grow-plant\",\n        43\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        164\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        411\n    ],\n    [\n        \"task-3-find-living-thing\",\n        106\n    ],\n    [\n        \"task-10-use-thermometer\",\n        163\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        194\n    ],\n    [\n        \"task-3-find-animal\",\n        114\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        2\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        279\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        426\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        281\n    ],\n    [\n        \"task-4-grow-plant\",\n        53\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        55\n    ],\n    [\n        \"task-3-find-plant\",\n        173\n    ],\n    [\n        \"task-3-find-plant\",\n        11\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        180\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        42\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        163\n    ],\n    [\n        \"task-3-find-living-thing\",\n        120\n    ],\n    [\n        \"task-10-use-thermometer\",\n        234\n    ],\n    [\n        \"task-10-use-thermometer\",\n        2\n    ],\n    [\n        \"task-10-use-thermometer\",\n        212\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        6\n    ],\n    [\n        \"task-3-find-animal\",\n        93\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        125\n    ],\n    [\n        \"task-10-use-thermometer\",\n        63\n    ],\n    [\n        \"task-3-find-animal\",\n        127\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        11\n    ],\n    [\n        \"task-10-use-thermometer\",\n        208\n    ],\n    [\n        \"task-3-find-animal\",\n        67\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        13\n    ],\n    [\n        \"task-10-use-thermometer\",\n        24\n    ],\n    [\n        \"task-3-find-plant\",\n        67\n    ],\n    [\n        \"task-1-boil\",\n        9\n    ],\n    [\n        \"task-3-find-plant\",\n        63\n    ],\n    [\n        \"task-3-find-plant\",\n        200\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        69\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        76\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        208\n    ],\n    [\n        \"task-3-find-animal\",\n        28\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        82\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        39\n    ],\n    [\n        \"task-3-find-plant\",\n        142\n    ],\n    [\n        \"task-3-find-plant\",\n        133\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        18\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        326\n    ],\n    [\n        \"task-3-find-animal\",\n        141\n    ],\n    [\n        \"task-1-freeze\",\n        2\n    ],\n    [\n        \"task-3-find-animal\",\n        219\n    ],\n    [\n        \"task-3-find-animal\",\n        135\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        84\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        44\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        11\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        65\n    ],\n    [\n        \"task-3-find-living-thing\",\n        2\n    ],\n    [\n        \"task-3-find-living-thing\",\n        100\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        79\n    ],\n    [\n        \"task-3-find-animal\",\n        115\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        244\n    ],\n    [\n        \"task-3-find-living-thing\",\n        167\n    ],\n    [\n        \"task-3-find-living-thing\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        92\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        209\n    ],\n    [\n        \"task-3-find-plant\",\n        23\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        42\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        8\n    ],\n    [\n        \"task-3-find-plant\",\n        219\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        280\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        200\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        221\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        229\n    ],\n    [\n        \"task-3-find-plant\",\n        9\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        4\n    ],\n    [\n        \"task-3-find-plant\",\n        175\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        320\n    ],\n    [\n        \"task-3-find-living-thing\",\n        193\n    ],\n    [\n        \"task-3-find-plant\",\n        35\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        184\n    ],\n    [\n        \"task-3-find-animal\",\n        151\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        32\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        148\n    ],\n    [\n        \"task-3-find-animal\",\n        42\n    ],\n    [\n        \"task-3-find-animal\",\n        112\n    ],\n    [\n        \"task-3-find-animal\",\n        121\n    ],\n    [\n        \"task-4-grow-fruit\",\n        42\n    ],\n    [\n        \"task-1-melt\",\n        3\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        75\n    ],\n    [\n        \"task-3-find-animal\",\n        158\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        17\n    ],\n    [\n        \"task-3-find-animal\",\n        125\n    ],\n    [\n        \"task-3-find-plant\",\n        118\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        208\n    ],\n    [\n        \"task-3-find-plant\",\n        143\n    ],\n    [\n        \"task-3-find-animal\",\n        90\n    ],\n    [\n        \"task-3-find-living-thing\",\n        157\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        205\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        60\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        19\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        11\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        73\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        11\n    ],\n    [\n        \"task-3-find-animal\",\n        195\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        54\n    ],\n    [\n        \"task-3-find-living-thing\",\n        49\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        100\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        287\n    ],\n    [\n        \"task-4-grow-plant\",\n        6\n    ],\n    [\n        \"task-3-find-animal\",\n        136\n    ],\n    [\n        \"task-4-grow-plant\",\n        9\n    ],\n    [\n        \"task-3-find-animal\",\n        200\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        80\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        252\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        15\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        41\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        18\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        62\n    ],\n    [\n        \"task-3-find-plant\",\n        32\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        160\n    ],\n    [\n        \"task-3-find-plant\",\n        100\n    ],\n    [\n        \"task-3-find-animal\",\n        210\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        15\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        305\n    ],\n    [\n        \"task-3-find-plant\",\n        167\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        37\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        243\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        48\n    ],\n    [\n        \"task-3-find-animal\",\n        164\n    ],\n    [\n        \"task-3-find-living-thing\",\n        199\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        81\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        29\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        121\n    ],\n    [\n        \"task-3-find-animal\",\n        221\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        52\n    ],\n    [\n        \"task-3-find-plant\",\n        90\n    ],\n    [\n        \"task-3-find-living-thing\",\n        34\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        0\n    ],\n    [\n        \"task-3-find-living-thing\",\n        84\n    ],\n    [\n        \"task-1-melt\",\n        7\n    ],\n    [\n        \"task-3-find-animal\",\n        24\n    ],\n    [\n        \"task-4-grow-fruit\",\n        31\n    ],\n    [\n        \"task-1-boil\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        141\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        1\n    ],\n    [\n        \"task-3-find-plant\",\n        123\n    ],\n    [\n        \"task-4-grow-plant\",\n        0\n    ],\n    [\n        \"task-4-grow-fruit\",\n        27\n    ],\n    [\n        \"task-3-find-living-thing\",\n        70\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        85\n    ],\n    [\n        \"task-3-find-living-thing\",\n        160\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        132\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        22\n    ],\n    [\n        \"task-4-grow-fruit\",\n        36\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        2\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        222\n    ],\n    [\n        \"task-3-find-living-thing\",\n        222\n    ],\n    [\n        \"task-3-find-animal\",\n        188\n    ],\n    [\n        \"task-4-grow-plant\",\n        61\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        111\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        10\n    ],\n    [\n        \"task-4-grow-plant\",\n        3\n    ],\n    [\n        \"task-3-find-plant\",\n        128\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        9\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        56\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        14\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        48\n    ],\n    [\n        \"task-3-find-plant\",\n        217\n    ],\n    [\n        \"task-4-grow-fruit\",\n        46\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        84\n    ],\n    [\n        \"task-3-find-plant\",\n        106\n    ],\n    [\n        \"task-3-find-living-thing\",\n        180\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        64\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        0\n    ],\n    [\n        \"task-3-find-animal\",\n        23\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        54\n    ],\n    [\n        \"task-3-find-living-thing\",\n        77\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        150\n    ],\n    [\n        \"task-3-find-animal\",\n        132\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        1\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        13\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        238\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        0\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        113\n    ],\n    [\n        \"task-3-find-plant\",\n        182\n    ],\n    [\n        \"task-1-freeze\",\n        8\n    ],\n    [\n        \"task-3-find-living-thing\",\n        188\n    ],\n    [\n        \"task-3-find-living-thing\",\n        18\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        75\n    ],\n    [\n        \"task-4-grow-plant\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        51\n    ],\n    [\n        \"task-3-find-living-thing\",\n        32\n    ],\n    [\n        \"task-3-find-plant\",\n        176\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        13\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        189\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        37\n    ],\n    [\n        \"task-1-freeze\",\n        0\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        90\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        216\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        182\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        83\n    ],\n    [\n        \"task-3-find-living-thing\",\n        115\n    ],\n    [\n        \"task-3-find-living-thing\",\n        66\n    ],\n    [\n        \"task-3-find-plant\",\n        88\n    ],\n    [\n        \"task-4-grow-plant\",\n        55\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        224\n    ],\n    [\n        \"task-3-find-plant\",\n        117\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        195\n    ],\n    [\n        \"task-3-find-plant\",\n        76\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        1\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        77\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        174\n    ],\n    [\n        \"task-3-find-living-thing\",\n        214\n    ],\n    [\n        \"task-3-find-living-thing\",\n        216\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        71\n    ],\n    [\n        \"task-3-find-plant\",\n        17\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        91\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        44\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        204\n    ],\n    [\n        \"task-1-boil\",\n        2\n    ],\n    [\n        \"task-3-find-plant\",\n        208\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        11\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        219\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        93\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        80\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        62\n    ],\n    [\n        \"task-3-find-living-thing\",\n        81\n    ],\n    [\n        \"task-3-find-living-thing\",\n        122\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        316\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        7\n    ],\n    [\n        \"task-3-find-living-thing\",\n        159\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        14\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        139\n    ],\n    [\n        \"task-3-find-living-thing\",\n        102\n    ],\n    [\n        \"task-4-grow-plant\",\n        27\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        64\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        51\n    ],\n    [\n        \"task-4-grow-plant\",\n        36\n    ],\n    [\n        \"task-3-find-living-thing\",\n        27\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        3\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        37\n    ],\n    [\n        \"task-3-find-living-thing\",\n        187\n    ],\n    [\n        \"task-3-find-animal\",\n        153\n    ],\n    [\n        \"task-4-grow-plant\",\n        85\n    ],\n    [\n        \"task-3-find-animal\",\n        39\n    ],\n    [\n        \"task-3-find-animal\",\n        157\n    ],\n    [\n        \"task-3-find-animal\",\n        77\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        33\n    ],\n    [\n        \"task-4-grow-plant\",\n        45\n    ],\n    [\n        \"task-3-find-animal\",\n        89\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        32\n    ],\n    [\n        \"task-3-find-living-thing\",\n        204\n    ],\n    [\n        \"task-3-find-animal\",\n        161\n    ],\n    [\n        \"task-3-find-plant\",\n        116\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        23\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        3\n    ],\n    [\n        \"task-3-find-animal\",\n        165\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        46\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        58\n    ],\n    [\n        \"task-4-grow-fruit\",\n        29\n    ],\n    [\n        \"task-3-find-animal\",\n        170\n    ],\n    [\n        \"task-3-find-plant\",\n        81\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        167\n    ],\n    [\n        \"task-3-find-animal\",\n        182\n    ],\n    [\n        \"task-4-grow-plant\",\n        91\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        60\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        31\n    ],\n    [\n        \"task-3-find-plant\",\n        109\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        11\n    ],\n    [\n        \"task-3-find-animal\",\n        100\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        91\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        20\n    ],\n    [\n        \"task-4-grow-fruit\",\n        12\n    ],\n    [\n        \"task-3-find-plant\",\n        84\n    ],\n    [\n        \"task-3-find-living-thing\",\n        0\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        14\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        7\n    ],\n    [\n        \"task-4-grow-plant\",\n        42\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        64\n    ],\n    [\n        \"task-3-find-plant\",\n        132\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        13\n    ],\n    [\n        \"task-4-grow-plant\",\n        56\n    ],\n    [\n        \"task-3-find-plant\",\n        99\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        28\n    ],\n    [\n        \"task-4-grow-plant\",\n        22\n    ],\n    [\n        \"task-3-find-living-thing\",\n        201\n    ],\n    [\n        \"task-4-grow-plant\",\n        44\n    ],\n    [\n        \"task-3-find-animal\",\n        31\n    ],\n    [\n        \"task-4-grow-fruit\",\n        87\n    ],\n    [\n        \"task-3-find-animal\",\n        137\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        79\n    ],\n    [\n        \"task-3-find-living-thing\",\n        119\n    ],\n    [\n        \"task-3-find-animal\",\n        177\n    ],\n    [\n        \"task-3-find-living-thing\",\n        219\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        33\n    ],\n    [\n        \"task-3-find-plant\",\n        66\n    ],\n    [\n        \"task-3-find-living-thing\",\n        200\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        24\n    ],\n    [\n        \"task-3-find-living-thing\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        7\n    ],\n    [\n        \"task-3-find-plant\",\n        137\n    ],\n    [\n        \"task-1-melt\",\n        13\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        22\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        25\n    ],\n    [\n        \"task-3-find-plant\",\n        108\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        4\n    ],\n    [\n        \"task-3-find-living-thing\",\n        183\n    ],\n    [\n        \"task-3-find-animal\",\n        36\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        63\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        169\n    ],\n    [\n        \"task-3-find-living-thing\",\n        76\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        23\n    ],\n    [\n        \"task-3-find-plant\",\n        104\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        103\n    ],\n    [\n        \"task-3-find-living-thing\",\n        136\n    ],\n    [\n        \"task-4-grow-plant\",\n        31\n    ],\n    [\n        \"task-3-find-animal\",\n        75\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        10\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        25\n    ],\n    [\n        \"task-3-find-plant\",\n        171\n    ],\n    [\n        \"task-3-find-plant\",\n        34\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        4\n    ],\n    [\n        \"task-3-find-animal\",\n        95\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        21\n    ],\n    [\n        \"task-4-grow-fruit\",\n        47\n    ],\n    [\n        \"task-3-find-plant\",\n        144\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        30\n    ],\n    [\n        \"task-3-find-living-thing\",\n        217\n    ],\n    [\n        \"task-3-find-living-thing\",\n        209\n    ],\n    [\n        \"task-4-grow-fruit\",\n        9\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        148\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        3\n    ],\n    [\n        \"task-3-find-plant\",\n        77\n    ],\n    [\n        \"task-3-find-living-thing\",\n        176\n    ],\n    [\n        \"task-3-find-living-thing\",\n        118\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        86\n    ],\n    [\n        \"task-4-grow-fruit\",\n        0\n    ],\n    [\n        \"task-3-find-animal\",\n        102\n    ],\n    [\n        \"task-3-find-living-thing\",\n        91\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        34\n    ],\n    [\n        \"task-3-find-plant\",\n        50\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        45\n    ],\n    [\n        \"task-4-grow-plant\",\n        84\n    ],\n    [\n        \"task-3-find-living-thing\",\n        185\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        37\n    ],\n    [\n        \"task-3-find-animal\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        110\n    ],\n    [\n        \"task-1-boil\",\n        6\n    ],\n    [\n        \"task-3-find-animal\",\n        49\n    ],\n    [\n        \"task-3-find-plant\",\n        25\n    ],\n    [\n        \"task-3-find-plant\",\n        124\n    ],\n    [\n        \"task-3-find-animal\",\n        167\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        73\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        160\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        36\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        9\n    ],\n    [\n        \"task-3-find-plant\",\n        160\n    ],\n    [\n        \"task-3-find-living-thing\",\n        186\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        215\n    ],\n    [\n        \"task-4-grow-fruit\",\n        61\n    ],\n    [\n        \"task-3-find-animal\",\n        40\n    ],\n    [\n        \"task-3-find-animal\",\n        181\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        43\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        57\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        66\n    ],\n    [\n        \"task-3-find-animal\",\n        60\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        41\n    ],\n    [\n        \"task-3-find-living-thing\",\n        177\n    ],\n    [\n        \"task-3-find-plant\",\n        169\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        77\n    ],\n    [\n        \"task-4-grow-plant\",\n        76\n    ],\n    [\n        \"task-3-find-plant\",\n        3\n    ],\n    [\n        \"task-3-find-living-thing\",\n        46\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        90\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        63\n    ],\n    [\n        \"task-3-find-animal\",\n        134\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        36\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        56\n    ],\n    [\n        \"task-4-grow-plant\",\n        35\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        12\n    ],\n    [\n        \"task-4-grow-fruit\",\n        60\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        17\n    ],\n    [\n        \"task-3-find-plant\",\n        206\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        74\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        131\n    ],\n    [\n        \"task-4-grow-fruit\",\n        28\n    ],\n    [\n        \"task-3-find-animal\",\n        82\n    ],\n    [\n        \"task-3-find-living-thing\",\n        144\n    ],\n    [\n        \"task-1-boil\",\n        5\n    ],\n    [\n        \"task-3-find-animal\",\n        8\n    ],\n    [\n        \"task-3-find-animal\",\n        104\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        202\n    ],\n    [\n        \"task-3-find-living-thing\",\n        42\n    ],\n    [\n        \"task-4-grow-fruit\",\n        39\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        76\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        55\n    ],\n    [\n        \"task-3-find-animal\",\n        166\n    ],\n    [\n        \"task-3-find-animal\",\n        175\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        40\n    ],\n    [\n        \"task-3-find-plant\",\n        218\n    ],\n    [\n        \"task-3-find-plant\",\n        41\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        14\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        179\n    ],\n    [\n        \"task-3-find-living-thing\",\n        158\n    ],\n    [\n        \"task-3-find-plant\",\n        149\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        5\n    ],\n    [\n        \"task-4-grow-plant\",\n        50\n    ],\n    [\n        \"task-3-find-living-thing\",\n        71\n    ],\n    [\n        \"task-3-find-plant\",\n        83\n    ],\n    [\n        \"task-3-find-plant\",\n        86\n    ],\n    [\n        \"task-3-find-plant\",\n        92\n    ],\n    [\n        \"task-3-find-animal\",\n        206\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        49\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        32\n    ],\n    [\n        \"task-3-find-animal\",\n        216\n    ],\n    [\n        \"task-3-find-animal\",\n        203\n    ],\n    [\n        \"task-4-grow-plant\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        65\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        7\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        181\n    ],\n    [\n        \"task-3-find-plant\",\n        18\n    ],\n    [\n        \"task-3-find-living-thing\",\n        19\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        206\n    ],\n    [\n        \"task-3-find-plant\",\n        165\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        55\n    ],\n    [\n        \"task-3-find-plant\",\n        39\n    ],\n    [\n        \"task-4-grow-plant\",\n        86\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        50\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        97\n    ],\n    [\n        \"task-4-grow-fruit\",\n        13\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        90\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        70\n    ],\n    [\n        \"task-1-freeze\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        77\n    ],\n    [\n        \"task-3-find-animal\",\n        2\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        48\n    ],\n    [\n        \"task-3-find-living-thing\",\n        142\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        93\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        27\n    ],\n    [\n        \"task-3-find-animal\",\n        193\n    ],\n    [\n        \"task-3-find-living-thing\",\n        47\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        72\n    ],\n    [\n        \"task-3-find-plant\",\n        114\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        24\n    ],\n    [\n        \"task-3-find-living-thing\",\n        69\n    ],\n    [\n        \"task-3-find-plant\",\n        203\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        53\n    ],\n    [\n        \"task-3-find-animal\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        39\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        56\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        72\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        60\n    ],\n    [\n        \"task-3-find-animal\",\n        222\n    ],\n    [\n        \"task-4-grow-plant\",\n        28\n    ],\n    [\n        \"task-3-find-animal\",\n        218\n    ],\n    [\n        \"task-3-find-living-thing\",\n        107\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        10\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        153\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        2\n    ],\n    [\n        \"task-3-find-plant\",\n        147\n    ],\n    [\n        \"task-4-grow-fruit\",\n        83\n    ],\n    [\n        \"task-1-melt\",\n        5\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        172\n    ],\n    [\n        \"task-3-find-plant\",\n        16\n    ],\n    [\n        \"task-3-find-animal\",\n        138\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        68\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        26\n    ],\n    [\n        \"task-3-find-living-thing\",\n        133\n    ],\n    [\n        \"task-3-find-plant\",\n        87\n    ],\n    [\n        \"task-4-grow-fruit\",\n        33\n    ],\n    [\n        \"task-3-find-animal\",\n        180\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        5\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        107\n    ],\n    [\n        \"task-3-find-living-thing\",\n        152\n    ],\n    [\n        \"task-3-find-plant\",\n        33\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        4\n    ],\n    [\n        \"task-4-grow-fruit\",\n        66\n    ],\n    [\n        \"task-3-find-living-thing\",\n        203\n    ],\n    [\n        \"task-3-find-living-thing\",\n        169\n    ],\n    [\n        \"task-2-power-component\",\n        9\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        28\n    ],\n    [\n        \"task-3-find-plant\",\n        214\n    ],\n    [\n        \"task-4-grow-fruit\",\n        10\n    ],\n    [\n        \"task-3-find-living-thing\",\n        150\n    ],\n    [\n        \"task-4-grow-fruit\",\n        17\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        44\n    ],\n    [\n        \"task-4-grow-fruit\",\n        73\n    ],\n    [\n        \"task-3-find-living-thing\",\n        6\n    ],\n    [\n        \"task-3-find-animal\",\n        47\n    ],\n    [\n        \"task-3-find-animal\",\n        96\n    ],\n    [\n        \"task-3-find-animal\",\n        140\n    ],\n    [\n        \"task-3-find-living-thing\",\n        63\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        67\n    ],\n    [\n        \"task-3-find-animal\",\n        73\n    ],\n    [\n        \"task-3-find-living-thing\",\n        154\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        54\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        76\n    ],\n    [\n        \"task-4-grow-plant\",\n        8\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        79\n    ],\n    [\n        \"task-4-grow-fruit\",\n        35\n    ],\n    [\n        \"task-3-find-living-thing\",\n        98\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        46\n    ],\n    [\n        \"task-4-grow-fruit\",\n        72\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        6\n    ],\n    [\n        \"task-4-grow-fruit\",\n        14\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        27\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        34\n    ],\n    [\n        \"task-3-find-living-thing\",\n        132\n    ],\n    [\n        \"task-4-grow-fruit\",\n        16\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        6\n    ],\n    [\n        \"task-3-find-living-thing\",\n        72\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        4\n    ],\n    [\n        \"task-4-grow-fruit\",\n        69\n    ],\n    [\n        \"task-1-melt\",\n        4\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        42\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        63\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        83\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        31\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        26\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        84\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        2\n    ],\n    [\n        \"task-4-grow-plant\",\n        17\n    ],\n    [\n        \"task-4-grow-fruit\",\n        5\n    ],\n    [\n        \"task-4-grow-plant\",\n        80\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        5\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        92\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        8\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        1\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        5\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        0\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        61\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        2\n    ],\n    [\n        \"task-4-grow-plant\",\n        89\n    ],\n    [\n        \"task-4-grow-plant\",\n        19\n    ],\n    [\n        \"task-4-grow-fruit\",\n        43\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        76\n    ],\n    [\n        \"task-4-grow-fruit\",\n        68\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        67\n    ],\n    [\n        \"task-4-grow-fruit\",\n        64\n    ],\n    [\n        \"task-1-freeze\",\n        6\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        48\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        16\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        10\n    ],\n    [\n        \"task-4-grow-plant\",\n        58\n    ],\n    [\n        \"task-4-grow-plant\",\n        70\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        17\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        0\n    ],\n    [\n        \"task-4-grow-plant\",\n        92\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        24\n    ],\n    [\n        \"task-4-grow-plant\",\n        74\n    ],\n    [\n        \"task-4-grow-fruit\",\n        57\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        35\n    ],\n    [\n        \"task-4-grow-plant\",\n        39\n    ],\n    [\n        \"task-1-freeze\",\n        11\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        43\n    ],\n    [\n        \"task-4-grow-fruit\",\n        24\n    ],\n    [\n        \"task-4-grow-plant\",\n        13\n    ],\n    [\n        \"task-4-grow-plant\",\n        67\n    ],\n    [\n        \"task-4-grow-plant\",\n        72\n    ],\n    [\n        \"task-4-grow-fruit\",\n        80\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        49\n    ],\n    [\n        \"task-1-freeze\",\n        10\n    ],\n    [\n        \"task-4-grow-plant\",\n        73\n    ],\n    [\n        \"task-1-freeze\",\n        1\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        1\n    ],\n    [\n        \"task-4-grow-plant\",\n        49\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        45\n    ],\n    [\n        \"task-4-grow-plant\",\n        52\n    ],\n    [\n        \"task-4-grow-plant\",\n        18\n    ],\n    [\n        \"task-1-freeze\",\n        12\n    ],\n    [\n        \"task-4-grow-plant\",\n        83\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        45\n    ],\n    [\n        \"task-4-grow-fruit\",\n        50\n    ],\n    [\n        \"task-4-grow-fruit\",\n        18\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        13\n    ],\n    [\n        \"task-4-grow-plant\",\n        90\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        11\n    ],\n    [\n        \"task-4-grow-fruit\",\n        19\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        12\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        5\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        11\n    ],\n    [\n        \"task-1-boil\",\n        0\n    ],\n    [\n        \"task-1-boil\",\n        1\n    ],\n    [\n        \"task-1-melt\",\n        1\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        46\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        10\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        5\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        15\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        77\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        7\n    ],\n    [\n        \"task-4-grow-plant\",\n        30\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        29\n    ],\n    [\n        \"task-4-grow-plant\",\n        2\n    ],\n    [\n        \"task-4-grow-plant\",\n        24\n    ],\n    [\n        \"task-4-grow-plant\",\n        20\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        3\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        23\n    ],\n    [\n        \"task-4-grow-plant\",\n        10\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        31\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        55\n    ],\n    [\n        \"task-4-grow-plant\",\n        59\n    ],\n    [\n        \"task-2-power-component\",\n        0\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        6\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        91\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        5\n    ],\n    [\n        \"task-2-power-component\",\n        6\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        35\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        50\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        75\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        5\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        37\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        0\n    ],\n    [\n        \"task-4-grow-plant\",\n        47\n    ],\n    [\n        \"task-4-grow-plant\",\n        81\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        19\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        12\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        87\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        4\n    ],\n    [\n        \"task-4-grow-plant\",\n        37\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        12\n    ],\n    [\n        \"task-4-grow-plant\",\n        46\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        88\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        6\n    ],\n    [\n        \"task-4-grow-fruit\",\n        49\n    ],\n    [\n        \"task-4-grow-fruit\",\n        92\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        14\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        2\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        13\n    ],\n    [\n        \"task-4-grow-fruit\",\n        78\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        9\n    ],\n    [\n        \"task-4-grow-fruit\",\n        8\n    ],\n    [\n        \"task-4-grow-plant\",\n        57\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        53\n    ],\n    [\n        \"task-4-grow-fruit\",\n        48\n    ],\n    [\n        \"task-4-grow-plant\",\n        78\n    ],\n    [\n        \"task-4-grow-plant\",\n        66\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        71\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        67\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        11\n    ],\n    [\n        \"task-4-grow-fruit\",\n        90\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        9\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        0\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        72\n    ],\n    [\n        \"task-2-power-component\",\n        2\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        39\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        12\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        79\n    ],\n    [\n        \"task-4-grow-plant\",\n        15\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        12\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        13\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        25\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        83\n    ],\n    [\n        \"task-4-grow-plant\",\n        75\n    ],\n    [\n        \"task-4-grow-plant\",\n        62\n    ],\n    [\n        \"task-4-grow-fruit\",\n        76\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        33\n    ],\n    [\n        \"task-2-power-component\",\n        7\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        17\n    ],\n    [\n        \"task-1-freeze\",\n        3\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        47\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        38\n    ],\n    [\n        \"task-4-grow-fruit\",\n        34\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        17\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        82\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        56\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        3\n    ],\n    [\n        \"task-4-grow-plant\",\n        64\n    ],\n    [\n        \"task-4-grow-fruit\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        33\n    ],\n    [\n        \"task-1-melt\",\n        11\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        42\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        18\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        29\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        53\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        20\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        60\n    ],\n    [\n        \"task-4-grow-plant\",\n        34\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        58\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        43\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        4\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        32\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        51\n    ],\n    [\n        \"task-4-grow-fruit\",\n        15\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        9\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        8\n    ],\n    [\n        \"task-4-grow-fruit\",\n        91\n    ],\n    [\n        \"task-4-grow-plant\",\n        12\n    ],\n    [\n        \"task-1-boil\",\n        10\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        86\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        78\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        58\n    ],\n    [\n        \"task-1-boil\",\n        12\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        75\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        74\n    ],\n    [\n        \"task-4-grow-plant\",\n        23\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        18\n    ],\n    [\n        \"task-4-grow-fruit\",\n        45\n    ],\n    [\n        \"task-4-grow-plant\",\n        32\n    ],\n    [\n        \"task-1-melt\",\n        9\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        2\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        26\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        14\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        53\n    ],\n    [\n        \"task-1-melt\",\n        8\n    ],\n    [\n        \"task-2-power-component\",\n        8\n    ],\n    [\n        \"task-4-grow-plant\",\n        4\n    ],\n    [\n        \"task-1-freeze\",\n        13\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        59\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        1\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        30\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        63\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        59\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        6\n    ],\n    [\n        \"task-1-melt\",\n        6\n    ]\n]"
  },
  {
    "path": "envs/sciworld/data/valid_indices.json",
    "content": "[\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        418\n    ],\n    [\n        \"task-3-find-living-thing\",\n        186\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        326\n    ],\n    [\n        \"task-3-find-animal\",\n        193\n    ],\n    [\n        \"task-4-grow-plant\",\n        65\n    ],\n    [\n        \"task-3-find-plant\",\n        179\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        587\n    ],\n    [\n        \"task-3-find-animal\",\n        218\n    ],\n    [\n        \"task-10-use-thermometer\",\n        384\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        479\n    ],\n    [\n        \"task-3-find-animal\",\n        159\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        528\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        413\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        352\n    ],\n    [\n        \"task-10-use-thermometer\",\n        385\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        69\n    ],\n    [\n        \"task-3-find-animal\",\n        211\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        313\n    ],\n    [\n        \"task-3-find-plant\",\n        175\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        474\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        662\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        386\n    ],\n    [\n        \"task-1-melt\",\n        14\n    ],\n    [\n        \"task-3-find-plant\",\n        209\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        579\n    ],\n    [\n        \"task-10-use-thermometer\",\n        314\n    ],\n    [\n        \"task-1-boil\",\n        14\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        280\n    ],\n    [\n        \"task-10-use-thermometer\",\n        378\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        655\n    ],\n    [\n        \"task-3-find-living-thing\",\n        161\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        448\n    ],\n    [\n        \"task-10-use-thermometer\",\n        360\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        389\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        287\n    ],\n    [\n        \"task-10-use-thermometer\",\n        364\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        640\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        409\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        523\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        25\n    ],\n    [\n        \"task-10-use-thermometer\",\n        284\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        14\n    ],\n    [\n        \"task-2a-test-conductivity\",\n        485\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        286\n    ],\n    [\n        \"task-3-find-living-thing\",\n        180\n    ],\n    [\n        \"task-4-grow-fruit\",\n        85\n    ],\n    [\n        \"task-3-find-plant\",\n        177\n    ],\n    [\n        \"task-3-find-living-thing\",\n        209\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        394\n    ],\n    [\n        \"task-1-melt\",\n        18\n    ],\n    [\n        \"task-3-find-animal\",\n        173\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        281\n    ],\n    [\n        \"task-3-find-animal\",\n        221\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        14\n    ],\n    [\n        \"task-1-melt\",\n        16\n    ],\n    [\n        \"task-3-find-living-thing\",\n        216\n    ],\n    [\n        \"task-4-grow-fruit\",\n        90\n    ],\n    [\n        \"task-2a-test-conductivity-of-unknown-substances\",\n        376\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        87\n    ],\n    [\n        \"task-3-find-animal\",\n        213\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        300\n    ],\n    [\n        \"task-3-find-plant\",\n        203\n    ],\n    [\n        \"task-10-use-thermometer\",\n        285\n    ],\n    [\n        \"task-3-find-animal\",\n        216\n    ],\n    [\n        \"task-3-find-animal\",\n        188\n    ],\n    [\n        \"task-3-find-animal\",\n        176\n    ],\n    [\n        \"task-3-find-plant\",\n        206\n    ],\n    [\n        \"task-4-grow-plant\",\n        77\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        253\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        87\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        299\n    ],\n    [\n        \"task-10-use-thermometer\",\n        358\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        292\n    ],\n    [\n        \"task-10-use-thermometer\",\n        291\n    ],\n    [\n        \"task-3-find-living-thing\",\n        214\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        67\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        16\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        83\n    ],\n    [\n        \"task-3-find-living-thing\",\n        221\n    ],\n    [\n        \"task-10-measure-melting-point-(known-substance)\",\n        325\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        91\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        84\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        150\n    ],\n    [\n        \"task-4-grow-plant\",\n        84\n    ],\n    [\n        \"task-4-grow-plant\",\n        68\n    ],\n    [\n        \"task-3-find-plant\",\n        181\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        76\n    ],\n    [\n        \"task-3-find-living-thing\",\n        202\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        7\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        88\n    ],\n    [\n        \"task-3-find-living-thing\",\n        206\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        184\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        68\n    ],\n    [\n        \"task-3-find-living-thing\",\n        218\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        187\n    ],\n    [\n        \"task-1-boil\",\n        15\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        84\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        66\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        214\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        25\n    ],\n    [\n        \"task-2-power-component\",\n        11\n    ],\n    [\n        \"task-3-find-plant\",\n        164\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        66\n    ],\n    [\n        \"task-3-find-plant\",\n        154\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        198\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        68\n    ],\n    [\n        \"task-3-find-plant\",\n        165\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        209\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        185\n    ],\n    [\n        \"task-1-boil\",\n        20\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        21\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        80\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        64\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        179\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        78\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        192\n    ],\n    [\n        \"task-1-boil\",\n        17\n    ],\n    [\n        \"task-1-melt\",\n        20\n    ],\n    [\n        \"task-4-grow-fruit\",\n        72\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        69\n    ],\n    [\n        \"task-3-find-non-living-thing\",\n        158\n    ],\n    [\n        \"task-4-grow-plant\",\n        64\n    ],\n    [\n        \"task-4-grow-fruit\",\n        88\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        17\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        80\n    ],\n    [\n        \"task-4-grow-plant\",\n        85\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        80\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        82\n    ],\n    [\n        \"task-4-grow-plant\",\n        86\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        18\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        26\n    ],\n    [\n        \"task-4-grow-plant\",\n        78\n    ],\n    [\n        \"task-1-melt\",\n        19\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        21\n    ],\n    [\n        \"task-4-grow-fruit\",\n        65\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        72\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        20\n    ],\n    [\n        \"task-4-grow-plant\",\n        88\n    ],\n    [\n        \"task-1-melt\",\n        15\n    ],\n    [\n        \"task-6-lifespan-(longest-lived-then-shortest-lived)\",\n        85\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        78\n    ],\n    [\n        \"task-4-grow-plant\",\n        87\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        23\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        70\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        85\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        72\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        19\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        18\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        73\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        15\n    ],\n    [\n        \"task-4-grow-fruit\",\n        84\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        19\n    ],\n    [\n        \"task-6-lifespan-(longest-lived)\",\n        92\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        22\n    ],\n    [\n        \"task-4-grow-fruit\",\n        80\n    ],\n    [\n        \"task-4-grow-fruit\",\n        71\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        5\n    ],\n    [\n        \"task-7-identify-life-stages-2\",\n        4\n    ],\n    [\n        \"task-1-freeze\",\n        19\n    ],\n    [\n        \"task-6-lifespan-(shortest-lived)\",\n        71\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        10\n    ],\n    [\n        \"task-1-freeze\",\n        15\n    ],\n    [\n        \"task-4-grow-fruit\",\n        62\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        26\n    ],\n    [\n        \"task-1-freeze\",\n        18\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        8\n    ],\n    [\n        \"task-4-grow-fruit\",\n        68\n    ],\n    [\n        \"task-7-identify-life-stages-1\",\n        6\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        18\n    ],\n    [\n        \"task-1-freeze\",\n        16\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        23\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        24\n    ],\n    [\n        \"task-1-melt\",\n        17\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        22\n    ],\n    [\n        \"task-1-boil\",\n        16\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        19\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        18\n    ],\n    [\n        \"task-2-power-component\",\n        13\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        23\n    ],\n    [\n        \"task-2-power-component\",\n        12\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        19\n    ],\n    [\n        \"task-2-power-component-(renewable-vs-nonrenewable-energy)\",\n        13\n    ],\n    [\n        \"task-1-freeze\",\n        17\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        21\n    ],\n    [\n        \"task-1-boil\",\n        19\n    ],\n    [\n        \"task-1-freeze\",\n        14\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        20\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(secondary-color)\",\n        24\n    ],\n    [\n        \"task-1-freeze\",\n        20\n    ],\n    [\n        \"task-1-change-the-state-of-matter-of\",\n        20\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        17\n    ],\n    [\n        \"task-5-chemistry-mix-paint-(tertiary-color)\",\n        20\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        16\n    ],\n    [\n        \"task-5-chemistry-mix\",\n        22\n    ]\n]"
  },
  {
    "path": "envs/sciworld/env.py",
    "content": "import contextlib\nimport json\nimport os\nimport random\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional, Union\nimport numpy as np\n\nimport yaml\nfrom base.environment import Env\nfrom utils.errors import StepLimitError\n\nclass SciWorldEnv(Env):\n    \"\"\"\n    An environment wrapper for ScienceWorld to conform to the base `Env` interface.\n    \"\"\"\n    env_name = \"sciworld\"\n    # Shared cache per data_root_dir to avoid repeated file reads across instances\n    _shared_cache: Dict[str, Dict[str, Any]] = {}\n\n    def __init__(\n        self,\n        config_path: str = \"envs/sciworld/base_config.yaml\",\n        simplification: str = \"easy\",\n        logger: Optional[Any] = None,\n    ) -> None:\n        self.simplification = simplification\n        self.logger = logger\n        self.config = {}\n        if config_path and Path(config_path).exists():\n            with open(config_path, 'r') as f:\n                self.config = yaml.safe_load(f) or {}\n\n        self.data_root_dir = Path(self.config['data_root_dir'])\n        root_key = str(self.data_root_dir.resolve())\n        cache = SciWorldEnv._shared_cache.get(root_key)\n        if cache is None:\n            cache = {\n                \"taskname2id\": json.load(open(self.data_root_dir / \"taskname2id.json\")),\n                \"max_steps\": json.load(open(self.data_root_dir / \"max_steps.json\")),\n                \"indices_by_split\": {},\n            }\n            SciWorldEnv._shared_cache[root_key] = cache\n        self.taskname2id = cache[\"taskname2id\"]\n        self.max_steps_dict = cache[\"max_steps\"]\n\n    def _initialize(self) -> None:\n        \"\"\"Initialize the ScienceWorld environment.\"\"\"\n        if self.logger:\n            self.logger.info(\"Initializing ScienceWorld environment\")\n        try:\n            import scienceworld\n        except ImportError as e:\n            raise ImportError(\n                \"The 'scienceworld' library is required to use SciWorldEnv. \"\n                \"Please install it with 'pip install scienceworld'.\"\n            ) from e\n\n        # Suppress verbose output from the ScienceWorld library\n        with open(os.devnull, \"w\") as devnull, contextlib.redirect_stdout(devnull):\n            self.env = scienceworld.ScienceWorldEnv(\n                serverPath=None,\n                envStepLimit=np.inf\n            )\n\n        if self.logger:\n            self.logger.info(\"ScienceWorld environment initialized successfully\")\n\n    def _load_indices(self, split: str, seed: int) -> List[int]:\n        \"\"\"Load the indices for the given split (cached across instances).\"\"\"\n        root_key = str(self.data_root_dir.resolve())\n        cache = SciWorldEnv._shared_cache.get(root_key)\n        if cache is None:\n            cache = {\n                \"taskname2id\": getattr(self, \"taskname2id\", {}),\n                \"max_steps\": getattr(self, \"max_steps_dict\", {}),\n                \"indices_by_split\": {},\n            }\n            SciWorldEnv._shared_cache[root_key] = cache\n        indices_cache = cache.setdefault(\"indices_by_split\", {})\n        if split not in indices_cache:\n            if split == \"validation\":\n                split = \"valid\"\n            with open(self.data_root_dir / f\"{split}_indices.json\", \"r\") as f:\n                indices_cache[split] = json.load(f)\n\n        random.seed(seed)\n        random.shuffle(indices_cache[split])\n        return indices_cache[split]\n\n    def reset(self, running_config: dict, id: Optional[str] = None) -> dict:\n        \"\"\"\n        Reset environment to its initial state.\n        The `id` can be used to set the variation for the task.\n        \"\"\"\n        self._initialize()\n        if self.env is None:\n            raise RuntimeError(\"Environment could not be initialized.\")\n        \n        self.split = running_config.get(\"split\", \"train\")\n        seed = running_config.get(\"seed\", 42)\n        self.indices = self._load_indices(self.split, seed)\n\n        id_int: Optional[int] = None\n        if id is not None:\n            try:\n                id_int = int(id)\n            except ValueError:\n                raise ValueError(f\"Task ID '{id}' is not a valid integer.\")\n            if not 0 <= id_int < len(self.indices):\n                raise ValueError(\n                    f\"Task ID {id_int} is out of valid range (0-{len(self.indices) - 1}).\"\n                )\n            self.task_name, self.variation = self.indices[id_int]\n\n        self.id = f\"{self.taskname2id[self.task_name]}_{self.variation}\"\n        self.max_steps = self.max_steps_dict[self.task_name]\n\n        self.env.load(self.task_name, self.variation, self.simplification, generateGoldPath=False)\n        obs, info = self.env.reset()\n\n        self._step_count = 0\n        self._done = False\n        self._success = False\n        self._reward = 0.0\n        task_description = info.get(\"taskDesc\", \"No task description found.\")\n        observation = f\"{task_description}\\n{obs}\"\n\n        return {\"observations\": [observation], \"env_name\": self.env_name, \"env\": self}\n\n    async def _run(self, single_action: str) -> str:\n        \"\"\"Execute one action against the underlying ScienceWorld env.\"\"\"\n\n        if not single_action:\n            return \"\"\n\n        # Early exit if already terminated\n        if self._done:\n            return \"The environment has already terminated.\"\n\n        if single_action.strip().lower() == \"[finish]\":\n            self._done = True\n            return \"You have finished the task.\" if self._success else \"Task failed.\"\n\n        # Increment step counter before executing\n        self._step_count += 1\n        if self.max_steps and self._step_count > self.max_steps:\n            self._done = True\n            raise StepLimitError(f\"Step limit of {self.max_steps} exceeded.\")\n\n        try:\n            obs, _, done, info = self.env.step(single_action)\n            self.logger.info(f\"[Score] {info['score']}\")\n            # self.logger.info(f\"[Info]: {info}\")\n            self._reward = info['score'] if info['score'] is not None and info['score'] > self._reward else self._reward\n            self._done = done\n            if info['score'] > 0 and done:\n                self._success = True\n            return obs\n        except Exception as e:\n            if self.logger:\n                self.logger.error(f\"Error executing action '{single_action}': {e}\")\n            self._done = True\n            self._success = False\n            raise\n\n    def is_done(self) -> bool:\n        \"\"\"Check if the environment is done.\"\"\"\n        return self._done\n\n    # Provide current step count for external callers (e.g., run.py)\n    def get_step_count(self) -> int:\n        return self._step_count\n\n    def is_success(self) -> bool:\n        \"\"\"Check if the task was successfully completed.\"\"\"\n        return self._success\n\n    def report(self):\n        return {\n            \"success\": self._success,\n            \"step\": self._step_count,\n            \"reward\": self._reward,\n            \"task_type\": self.task_name,\n        }\n\n    async def close(self) -> None:\n        \"\"\"Close the ScienceWorld environment and clean up resources.\"\"\"\n        if self.logger:\n            self.logger.info(\"Closing ScienceWorld environment\")\n        \n        try:\n            # Clean up the ScienceWorld environment if it exists\n            if hasattr(self, 'env') and self.env is not None:\n                # ScienceWorld doesn't have an explicit close method, but we can clean up references\n                self.env = None\n                \n            # Reset state variables\n            self._step_count = 0\n            self._done = False\n            self._success = False\n            self._reward = 0.0\n            \n            if self.logger:\n                self.logger.info(\"ScienceWorld environment closed successfully\")\n                \n        except Exception as e:\n            if self.logger:\n                self.logger.error(f\"Error closing ScienceWorld environment: {e}\")\n            raise"
  },
  {
    "path": "envs/webshop/env.py",
    "content": "import warnings\nwarnings.filterwarnings(\"ignore\", \"The 'text' argument to find\\\\(\\\\)-type methods is deprecated\", category=DeprecationWarning)\n\nfrom typing import Any, Optional\nfrom bs4 import BeautifulSoup\nfrom bs4.element import Comment\n\nfrom utils.common import read_json_file\n\nfrom base.environment import Env\nfrom envs.webshop.src.webshop.web_agent_site.envs import WebAgentTextEnv\nfrom envs.webshop.src.webshop.web_agent_site.utils import DEFAULT_FILE_PATH\nfrom envs.webshop.src.webshop.web_agent_site.envs.web_agent_text_env import SimServer\n\n_SHARED_SERVER = None\n\ndef clean_str(p):\n    \"\"\"Clean string encoding issues\"\"\"\n    return p.encode().decode(\"unicode-escape\").encode(\"latin1\").decode(\"utf-8\")\n\ndef tag_visible(element):\n    \"\"\"Check if HTML element should be visible in text conversion\"\"\"\n    ignore = {'style', 'script', 'head', 'title', 'meta', '[document]'}\n    return (\n        element.parent.name not in ignore and not isinstance(element, Comment)\n    )\n\ndef webshop_text(html_content, max_products=10):\n    \"\"\"Convert WebShop HTML to text with proper formatting.\n    \n    Args:\n        html_content: HTML content to parse\n        max_products: Maximum number of products to display (default: 10)\n    \"\"\"\n    try:\n        # Parse HTML content\n        html_obj = BeautifulSoup(html_content, 'html.parser')\n        texts = html_obj.find_all(string=True)\n        visible_texts = list(filter(tag_visible, texts))\n        \n        # Format text\n        observation = ''\n        option_type = ''\n        option_types = {}\n        asins = []\n        cnt = 0\n        prod_cnt = 0\n        just_prod = 0\n        \n        for t in visible_texts:\n            if t == '\\n': \n                continue\n            if t.replace('\\n', '').replace('\\\\n', '').replace(' ', '') == '': \n                continue\n                \n            if t.parent.name == 'button':  # button\n                processed_t = f'\\n[{t}] '\n            elif t.parent.name == 'label':  # options\n                processed_t = f'[{t}]'\n                option_types[str(t)] = option_type\n            elif t.parent.get('class') == [\"product-link\"]:  # product asins\n                processed_t = f'\\n[{t}] '\n                if prod_cnt >= max_products:\n                    processed_t = ''\n                prod_cnt += 1\n                asins.append(str(t))\n                just_prod = 0\n            else:  # regular, unclickable text\n                processed_t = '\\n' + str(t) + ' '\n                if cnt < 2: \n                    processed_t = ''\n                if just_prod <= 2 and prod_cnt >= max_products + 1: \n                    processed_t = ''\n                option_type = str(t)\n                cnt += 1\n            just_prod += 1\n            observation += processed_t\n        \n        # Build info dict\n        info = {}\n        if option_types:\n            info['option_types'] = option_types\n        if asins:\n            info['asins'] = asins\n        if 'Your score (min 0.0, max 1.0)' in visible_texts:\n            idx = visible_texts.index('Your score (min 0.0, max 1.0)')\n            info['reward'] = float(visible_texts[idx + 1])\n            observation = 'Your score (min 0.0, max 1.0): ' + str(visible_texts[idx + 1])\n        \n        return clean_str(observation), info\n        \n    except Exception as e:\n        # Fallback to basic format if parsing fails\n        return f\"HTML parsing error: {str(e)}\", {}\n\n# def _read_first_non_ws_char(file_path: Path) -> Optional[str]:\n#     try:\n#         with open(file_path, 'r', encoding='utf-8') as f:\n#             chunk = f.read(2048)\n#             for ch in chunk:\n#                 if not ch.isspace():\n#                     return ch\n#     except Exception:\n#         return None\n#     return None\n\ndef _get_shared_server(\n    file_path: Optional[str],\n    num_products: Optional[int],\n    human_goals: bool,\n    limit_goals: int = -1,\n    quiet: bool = False,\n):\n    global _SHARED_SERVER\n    if _SHARED_SERVER is None:\n        _SHARED_SERVER = SimServer(\n            base_url='http://127.0.0.1:3000',\n            file_path=file_path,\n            filter_goals=None,\n            limit_goals=limit_goals,\n            num_products=num_products,\n            human_goals=human_goals,\n            show_attrs=False,\n            quiet=quiet,\n        )\n    return _SHARED_SERVER\n\nclass WebShopEnv(Env):\n    \"\"\"WebShop Environment for agent interaction.\"\"\"\n    env_name = \"webshop\"\n    def __init__(\n        self,\n        logger: Optional[Any] = None,\n        max_steps: int = 30,\n        file_path: Optional[str] = DEFAULT_FILE_PATH,\n        success_threshold: float = 1.0,\n    ):\n        self.logger = logger\n        self.max_steps = max_steps\n        self.success_threshold = success_threshold\n        \n        # Initialize environment state\n        self.id = \"webshop_env\"\n        self._step_count = 0\n        self.is_finished = False\n        self.reward = 0.0\n        self.last_observation = \"\"\n        self.last_raw_observation = \"\"  # Store raw observation\n        self.current_session = None\n        self.trajectory = []  # Store complete trajectory like human agent\n        \n\n\n        # Use a shared SimServer to avoid reloading data per instance\n        self._server = _get_shared_server(\n            file_path=file_path,\n            num_products=None,\n            human_goals=True,\n            quiet=True,\n        )\n\n        self.webshop_env = WebAgentTextEnv(\n            observation_mode=\"text\",\n            server=self._server,\n            num_products=None,\n            human_goals=True,\n            quiet=True,\n        )\n        \n        if self.logger:\n            self.logger.info(f\"WebShop environment initialized\")\n            self.logger.info(f\"Configuration: max_steps={self.max_steps}, \"\n                           f\"success_threshold={self.success_threshold}\")\n    \n    \n    def _ensure_session_asins(self):\n        \"\"\"Ensure user session has proper asins field (fix for product click bug)\"\"\"\n        session_id = self.webshop_env.session\n        if hasattr(self.webshop_env, 'server') and session_id in self.webshop_env.server.user_sessions:\n            session = self.webshop_env.server.user_sessions[session_id]\n            if 'asins' not in session:\n                session['asins'] = set()\n            elif not isinstance(session['asins'], set):\n                # Convert list to set if needed\n                session['asins'] = set(session['asins']) if hasattr(session['asins'], '__iter__') else set()\n    \n    def reset(self, running_config: dict, id: Optional[str] = None):\n        \"\"\"Reset the environment using official WebShop reset\"\"\"\n        if self.logger:\n            self.logger.info(f\"Resetting WebShop environment (ID: {id})\")\n            \n        self._step_count = 0\n        self.is_finished = False\n        self.reward = 0.0\n        self.trajectory = []  # Reset trajectory\n\n        self.split = running_config.get(\"split\", \"train\")\n\n        if self.split == \"train\":\n            self.indices = read_json_file(f\"envs/webshop/data/train_indices.json\")\n        elif self.split == \"test\":\n            self.indices = read_json_file(f\"envs/webshop/data/test_indices.json\")\n        else:\n            raise ValueError(f\"Invalid split: {self.split}. WebShop has only train and test splits.\")\n        \n        # Use official WebShop reset\n        self.id = id\n        id_int: Optional[int] = None\n\n        if id is not None:\n            try:\n                id_int = int(id)\n            except ValueError:\n                raise ValueError(f\"Task ID '{id}' is not a valid integer.\")\n\n        self.session_id = self.indices[id_int]\n        result = self.webshop_env.reset(session=self.session_id)\n        \n        # Handle both tuple (observation, info) and single observation return\n        if isinstance(result, tuple):\n            observation, info = result\n        else:\n            observation = result\n            info = {}\n        \n        # Ensure user session has proper asins field\n        self._ensure_session_asins()\n        \n        # Store raw observation first\n        self.last_raw_observation = observation if observation else \"No observation available\"\n        \n        # Now format for agent\n        formatted_observation = self._format_observation(observation)\n        self.last_observation = formatted_observation\n        self.current_session = self.webshop_env.session\n        \n        # Create trajectory entry in human agent style\n        trajectory_entry = {\n            \"action\": None,  # No action for reset\n            \"observation\": formatted_observation,\n            \"raw_observation\": self.last_raw_observation,\n            \"url\": \"http://127.0.0.1:3000\",  # WebShop base URL\n            \"goal\": self.webshop_env.get_instruction_text(),\n            \"step\": self._step_count,\n            \"session\": self.current_session,\n            \"reward\": 0.0,\n            \"info\": info\n        }\n        self.trajectory.append(trajectory_entry)\n        \n        if self.logger:\n            self.logger.info(f\"WebShop reset with session {self.current_session}\")\n        \n        return {\"observations\": [formatted_observation], \"env_name\": self.env_name, \"env\": self}\n    \n    def _format_observation(self, observation: str) -> str:\n        \"\"\"Format observation with proper text conversion.\"\"\"\n        if observation is None:\n            return \"No observation available\"\n        \n        try:\n            if hasattr(self.webshop_env, 'state') and self.webshop_env.state.get('html'):\n                html_content = self.webshop_env.state['html']\n                formatted_obs, info = webshop_text(html_content)\n                \n                if info and hasattr(self.webshop_env.server, 'user_sessions') and self.current_session:\n                    current_session_info = self.webshop_env.server.user_sessions.get(self.current_session, {})\n                    current_session_info.update(info)\n                \n                if formatted_obs and formatted_obs.strip():\n                    return formatted_obs\n        except Exception:\n            pass\n        \n        return observation.replace(' [SEP] ', '\\n')\n    \n    async def _run(self, action: str):\n        \"\"\"Execute an action using official WebShop step function\"\"\"\n        if self.is_finished:\n            if self.logger:\n                self.logger.warning(f\"Attempted action '{action}' on finished environment\")\n            return self.last_observation\n        \n        self._step_count += 1\n        \n        if self.logger:\n            self.logger.info(f\"Step {self._step_count}: {action}\")\n        \n        # Handle [FINISH] action - this terminates the episode\n        if action == \"[FINISH]\":\n            self.is_finished = True\n            if self.logger:\n                self.logger.info(f\"[FINISH] action received - terminating episode\")\n                self.logger.info(f\"Episode finished with final reward: {self.reward:.3f}, success: {self.is_success()}\")\n            \n            # Add finish action to trajectory\n            trajectory_entry = {\n                \"action\": action,\n                \"observation\": self.last_observation,\n                \"raw_observation\": self.last_raw_observation,\n                \"url\": \"FINISH\",\n                \"goal\": self.webshop_env.get_instruction_text(),\n                \"step\": self._step_count,\n                \"session\": self.current_session,\n                \"reward\": self.reward,\n                \"info\": {\"finish_action\": True}\n            }\n            self.trajectory.append(trajectory_entry)\n            \n            return self.last_observation\n        \n        # Check for step limit before execution\n        if self._step_count > self.max_steps:\n            self.is_finished = True\n            if self.logger:\n                self.logger.info(f\"Maximum steps ({self.max_steps}) reached. Episode terminated.\")\n            return self.last_observation\n        \n        # Use official WebShop step function\n        try:\n            # Ensure user session has proper asins field\n            self._ensure_session_asins()\n            \n            result = self.webshop_env.step(action)\n            \n            # Handle different return formats\n            if isinstance(result, tuple) and len(result) >= 4:\n                observation, reward, done, info = result[:4]\n            elif hasattr(result, '__iter__') and len(list(result)) >= 4:\n                observation, reward, done, info = list(result)[:4]\n            else:\n                # Fallback for unexpected return format\n                observation = str(result) if result is not None else \"No observation available\"\n                reward, done, info = 0.0, False, {}\n            \n            # Ensure observation is not None\n            if observation is None:\n                observation = \"No observation available\"\n            \n            # Store raw observation first\n            self.last_raw_observation = observation\n            \n            # Format observation for agent\n            formatted_observation = self._format_observation(observation)\n            self.last_observation = formatted_observation\n            self.reward = reward if reward is not None else 0.0\n            \n            # Add to trajectory in human agent style\n            try:\n                goal = self.webshop_env.get_instruction_text()\n            except:\n                goal = \"Episode completed\"\n            \n            trajectory_entry = {\n                \"action\": action,\n                \"observation\": formatted_observation,\n                \"raw_observation\": self.last_raw_observation,\n                \"url\": self._get_current_url(),\n                \"goal\": goal,\n                \"step\": self._step_count,\n                \"session\": self.current_session,\n                \"reward\": self.reward,\n                \"info\": info if info is not None else {}\n            }\n            self.trajectory.append(trajectory_entry)\n            \n            # WebShop has its own done condition when user clicks \"Buy Now\"\n            self.is_finished = done or self._step_count >= self.max_steps\n            \n            return formatted_observation\n            \n        except Exception as e:\n            self.is_finished = True\n            error_msg = f\"WebShop step execution failed: {str(e)}\"\n            \n            # Add error to trajectory\n            try:\n                goal = self.webshop_env.get_instruction_text()\n            except:\n                goal = \"Error occurred\"\n            \n            trajectory_entry = {\n                \"action\": action,\n                \"observation\": error_msg,\n                \"raw_observation\": error_msg,\n                \"url\": \"ERROR\",\n                \"goal\": goal,\n                \"step\": self._step_count,\n                \"session\": self.current_session,\n                \"reward\": 0.0,\n                \"info\": {\"error\": str(e)}\n            }\n            self.trajectory.append(trajectory_entry)\n            \n            if self.logger:\n                self.logger.error(f\"Step {self._step_count} ERROR: {error_msg}\")\n            \n            return error_msg\n    \n    def _get_current_url(self):\n        \"\"\"Get current URL based on WebShop state - simplified version\"\"\"\n        # This is a simplified version since we don't have direct access to WebShop's internal URL state\n        # In the real WebShop, this would be more detailed\n        base_url = \"http://127.0.0.1:3000\"\n        \n        # Try to infer URL from observation content\n        if \"search\" in self.last_observation.lower():\n            return f\"{base_url}/search\"\n        elif \"product\" in self.last_observation.lower() or \"Buy Now\" in self.last_observation:\n            return f\"{base_url}/item\"\n        else:\n            return base_url\n    \n    def is_done(self):\n        \"\"\"Check if the episode is done\"\"\"\n        return self.is_finished or self._step_count >= self.max_steps\n    \n    def is_success(self):\n        \"\"\"Check if the task was completed successfully\"\"\"\n        success = self.is_finished and self.reward >= self.success_threshold\n        \n        if self.logger and self.is_finished:\n            self.logger.info(f\"Task evaluation: reward={self.reward:.3f}, \"\n                           f\"threshold={self.success_threshold}, success={success}\")\n        \n        return success\n    \n    def get_step_count(self):\n        \"\"\"Get the current step count\"\"\"\n        return self._step_count\n    \n    def get_reward(self):\n        \"\"\"Get the current reward\"\"\"\n        return self.reward\n    \n    def get_available_actions(self):\n        \"\"\"Get available actions from official WebShop environment\"\"\"\n        return self.webshop_env.get_available_actions()\n    \n    def get_instruction_text(self):\n        \"\"\"Get current instruction text from official WebShop environment\"\"\"\n        return self.webshop_env.get_instruction_text()\n    \n    def get_trajectory(self):\n        \"\"\"Get the complete trajectory in human agent format\"\"\"\n        return self.trajectory\n    \n    async def close(self) -> None:\n        \"\"\"Close the official WebShop environment\"\"\"\n        if self.logger:\n            self.logger.info(f\"Closing WebShop environment. \"\n                           f\"Final stats: {len(self.trajectory)} trajectory steps, \"\n                           f\"final reward: {self.reward:.3f}, success: {self.is_success()}\")\n            \n            # Log trajectory summary for debugging\n            if self.trajectory:\n                self.logger.info(f\"Trajectory summary:\")\n                for i, step in enumerate(self.trajectory):\n                    action = step.get('action', 'RESET')\n                    reward = step.get('reward', 0.0)\n                    url = step.get('url', 'unknown')\n                    self.logger.info(f\"  {i}: '{action}' -> {url} -> reward={reward:.3f}\")\n        \n        try:\n            # Close the WebShop environment\n            if hasattr(self, 'webshop_env') and self.webshop_env:\n                self.webshop_env.close()\n            \n            # Clean up shared server if it's the last instance\n            global _SHARED_SERVER\n            if _SHARED_SERVER is not None:\n                try:\n                    _SHARED_SERVER = None\n                except Exception as e:\n                    if self.logger:\n                        self.logger.warning(f\"Error cleaning up shared server: {e}\")\n            \n            # Reset state variables\n            self._step_count = 0\n            self.is_finished = False\n            self.reward = 0.0\n            self.trajectory = []\n            self.current_session = None\n            self.last_observation = \"\"\n            self.last_raw_observation = \"\"\n            \n            if self.logger:\n                self.logger.info(\"WebShop environment closed successfully\")\n                \n        except Exception as e:\n            if self.logger:\n                self.logger.warning(f\"Error closing WebShop environment: {e}\")\n            raise\n\n    def report(self):\n        return {\n            \"success\": self.is_success(),\n            \"reward\": self.reward,\n            \"step\": self._step_count,\n        }"
  },
  {
    "path": "envs/webshop/setup.py",
    "content": "from setuptools import setup, find_packages\n\nsetup(\n    name='webshop',\n    version='0.1',\n    packages=find_packages('src'),\n    package_dir={'': 'src'},\n    install_requires=[\n        \"numpy==1.26.4\",\n        \"beautifulsoup4==4.11.1\",\n        \"cleantext==1.1.4\",\n        \"env==0.1.0\",\n        \"faiss-cpu==1.7.4\",\n        \"Flask==2.1.2\",\n        \"gym==0.24.0\",\n        \"pyserini==0.17.0\",\n        \"pytest\",\n        \"rank_bm25==0.2.2\",\n        \"requests_mock\",\n        \"scikit_learn==1.1.1\",\n        \"selenium==4.2.0\",\n        \"spacy==3.6.1\",\n        \"thinc==8.1.12\",\n        \"thefuzz==0.20.0\",\n        \"werkzeug==2.3.8\",\n    ],\n    author='Your Name',\n    author_email='youremail@example.com',\n    description='webshop pip package version',\n    license='MIT',\n    keywords='sample setuptools development',\n    url='https://github.com/yourusername/mypackage'\n)"
  },
  {
    "path": "envs/webshop/setup.sh",
    "content": "pip install gdown\ngdown https://drive.google.com/uc?id=1G_0ccLWn5kZE5rpeyAdh_YuoNzvBUjT9\ngdown https://drive.google.com/uc?id=11zOUDkJSgGhYin9NxQtG8PVpDsika86y\nunzip data.zip\nmkdir search_index\nunzip indexes.zip -d search_index/"
  },
  {
    "path": "envs/webshop/src/webshop/__init__.py",
    "content": ""
  },
  {
    "path": "envs/webshop/src/webshop/run_envs/run_web_agent_site_env.py",
    "content": "\"\"\"\nTest the site gym environment.\n\nTODO: move to testing dir for more rigorous tests\n\"\"\"\nimport gym\nfrom rich import print\nfrom rich.markup import escape\n\nfrom envs.webshop.src.webshop.web_agent_site.envs import WebAgentSiteEnv\nfrom envs.webshop.src.webshop.web_agent_site.models import (\n    HumanPolicy,\n    RandomPolicy,\n)\nfrom envs.webshop.src.webshop.web_agent_site.utils import DEBUG_PROD_SIZE\n\n\nif __name__ == '__main__':\n    #env = gym.make('WebAgentSite-v0')\n    #env = WebAgentSiteEnv(render=True, pause=2.0)\n    #env = WebAgentSiteEnv(observation_mode='html', render=False)\n    env = WebAgentSiteEnv(observation_mode='text', render=False, num_products=DEBUG_PROD_SIZE)\n    global_step = 0\n    \n    try:\n        #policy = HumanPolicy()\n        policy = RandomPolicy()\n    \n        observation = env.observation\n        while True:\n            print(observation)\n            available_actions = env.get_available_actions()\n            print('Available actions:', available_actions)\n            action = policy.forward(observation, available_actions)\n            observation, reward, done, info = env.step(action)\n            print(f'Taking action \"{escape(action)}\" -> Reward = {reward}')\n            if done:\n                break\n            global_step += 1\n    finally:\n        env.close()"
  },
  {
    "path": "envs/webshop/src/webshop/run_envs/run_web_agent_text_env.py",
    "content": "\"\"\"\nTest the text gym environment.\n\nTODO: move to testing dir for more rigorous tests\n\"\"\"\nimport gym\nfrom rich import print\nfrom rich.markup import escape\n\nfrom envs.webshop.src.webshop.web_agent_site.envs import WebAgentTextEnv\nfrom envs.webshop.src.webshop.web_agent_site.models import RandomPolicy\nfrom envs.webshop.src.webshop.web_agent_site.utils import DEBUG_PROD_SIZE\n\nif __name__ == '__main__':\n    env = gym.make('WebAgentTextEnv-v0', observation_mode='text', num_products=DEBUG_PROD_SIZE)\n    env.reset()\n    \n    try:\n        policy = RandomPolicy()\n    \n        observation = env.observation\n        while True:\n            print(observation)\n            available_actions = env.get_available_actions()\n            print('Available actions:', available_actions)\n            action = policy.forward(observation, available_actions)\n            observation, reward, done, info = env.step(action)\n            print(f'Taking action \"{escape(action)}\" -> Reward = {reward}')\n            if done:\n                break\n    finally:\n        env.close()"
  },
  {
    "path": "envs/webshop/src/webshop/search_engine/lucene_searcher.py",
    "content": "import json\nfrom pyserini.search.lucene import LuceneSearcher\nfrom rich import print\n\n\nsearcher = LuceneSearcher('indexes')\nhits = searcher.search('rubber sole shoes', k=20)\n\nfor hit in hits:\n    doc = searcher.doc(hit.docid)\n    print(doc)\n    obj = json.loads(doc.raw())['product']['Title']\n    print(obj)\n\nprint(len(hits))\n"
  },
  {
    "path": "envs/webshop/src/webshop/transfer/README.md",
    "content": "# Sim-to-real Transfer\nThis folder contains code for transferring agents trained on WebShop to perform on third party websites, specifically [Amazon](http://amazon.com) and [eBay](http://ebay.com). The imitation learning and reinforcement learning agents exercised by the transfer code can be found on WebShop's Hugging Face [page](https://huggingface.co/webshop).\n\nInteract with a demo of the transfer code, deployed as a 🤗 Hugging Face space [here](https://huggingface.co/spaces/webshop/amazon_shop)!\n\n## 🛠️ Usage\nThe Gradio app deployed as the aforementioned Hugging Face space can be started locally by running `python app.py` in this folder. The initial `setup.sh` script should have installed all the required dependencies.\n\n## ➡️ Transfer Logic\nThe Sim-to-real transfer code follows this general logical flow:\n\n<img src=\"../assets/transfer-logic.png\" width=\"100%\">\n\nThe contents of this directory each serve the following purposes:\n* `app.py`: Run to launch interactive [Gradio](https://gradio.app/) demo of app\n* `predict_help.py`: Amazon, eBay web scraping code\n* `webshop_lite.py`: A condensed version of WebShop's templating engine\n\nIf you are interested in *transferring an agent's functionality to an new website or platform*, you will need to...\n1. implement two new functions:  `parse_results_<platform>.py` and `parse_item_page_<platform>.py`. The corresponding interfaces and working examples for Amazon can be found [here](https://github.com/princeton-nlp/webshop/tree/master/transfer/predict_help.py#L262) and [here](https://github.com/princeton-nlp/webshop/tree/master/transfer/predict_help.py#L296).\n2. Invoke these functions in the [`run_episode`](https://github.com/princeton-nlp/webshop/tree/master/transfer/app.py#L105) function in the `app.py` file. Specifically, you should add a single call to...\n     * `parse_results...` in the [conditional]((https://github.com/princeton-nlp/webshop/tree/master/transfer/predict_help.py#L220)) handling `Page.RESULTS` page types\n     * `parse_item_page...` in the [conditional]((https://github.com/princeton-nlp/webshop/tree/master/transfer/predict_help.py#L240)) handling `Page.ITEMS` page types"
  },
  {
    "path": "envs/webshop/src/webshop/transfer/__init__.py",
    "content": ""
  },
  {
    "path": "envs/webshop/src/webshop/transfer/app.py",
    "content": "import gradio as gr\nimport json, time, torch\nfrom transformers import BartTokenizer, BartForConditionalGeneration, AutoModel, AutoTokenizer\n\nfrom .webshop_lite import dict_to_fake_html\nfrom .predict_help import (\n    Page, convert_dict_to_actions, convert_html_to_text,\n    parse_results_amz, parse_item_page_amz,\n    parse_results_ws, parse_item_page_ws,\n    parse_results_ebay, parse_item_page_ebay,\n    WEBSHOP_URL, WEBSHOP_SESSION\n)\n\nENVIRONMENTS = ['amazon', 'webshop', 'ebay']\n\n# IL+RL: 'webshop/il-rl-choice-bert-image_1'\n# IL: 'webshop/il-choice-bert-image_0'\nBERT_MODEL_PATH = 'webshop/il-choice-bert-image_0'\n\n# load IL models\nbart_tokenizer = BartTokenizer.from_pretrained('facebook/bart-large')\nbart_model = BartForConditionalGeneration.from_pretrained('webshop/il_search_bart')\n\nbert_tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased', truncation_side='left')\nbert_tokenizer.add_tokens(['[button]', '[button_]', '[clicked button]', '[clicked button_]'], special_tokens=True)\nbert_model = AutoModel.from_pretrained(BERT_MODEL_PATH, trust_remote_code=True)\n\ndef process_str(s):\n    s = s.lower().replace('\"', '').replace(\"'\", \"\").strip()\n    s = s.replace('[sep]', '[SEP]')\n    return s\n\n\ndef process_goal(state):\n    state = state.lower().replace('\"', '').replace(\"'\", \"\")\n    state = state.replace('amazon shopping game\\ninstruction:', '').replace('webshop\\ninstruction:', '')\n    state = state.replace('\\n[button] search [button_]', '').strip()\n    if ', and price lower than' in state:\n        state = state.split(', and price lower than')[0]\n    return state\n\n\ndef data_collator(batch):\n    state_input_ids, state_attention_mask, action_input_ids, action_attention_mask, sizes, labels, images = [], [], [], [], [], [], []\n    for sample in batch:\n        state_input_ids.append(sample['state_input_ids'])\n        state_attention_mask.append(sample['state_attention_mask'])\n        action_input_ids.extend(sample['action_input_ids'])\n        action_attention_mask.extend(sample['action_attention_mask'])\n        sizes.append(sample['sizes'])\n        labels.append(sample['labels'])\n        images.append(sample['images'])\n    max_state_len = max(sum(x) for x in state_attention_mask)\n    max_action_len = max(sum(x) for x in action_attention_mask)\n    return {\n        'state_input_ids': torch.tensor(state_input_ids)[:, :max_state_len],\n        'state_attention_mask': torch.tensor(state_attention_mask)[:, :max_state_len],\n        'action_input_ids': torch.tensor(action_input_ids)[:, :max_action_len],\n        'action_attention_mask': torch.tensor(action_attention_mask)[:, :max_action_len],\n        'sizes': torch.tensor(sizes),\n        'images': torch.tensor(images),\n        'labels': torch.tensor(labels),\n    }\n\n\ndef bart_predict(input):\n    input_ids = bart_tokenizer(input)['input_ids']\n    input_ids = torch.tensor(input_ids).unsqueeze(0)\n    output = bart_model.generate(input_ids, max_length=512, num_return_sequences=5, num_beams=5)\n    return bart_tokenizer.batch_decode(output.tolist(), skip_special_tokens=True)[0]\n\n\ndef bert_predict(obs, info, softmax=True):\n    valid_acts = info['valid']\n    assert valid_acts[0].startswith('click[')\n    state_encodings = bert_tokenizer(process_str(obs), max_length=512, truncation=True, padding='max_length')\n    action_encodings = bert_tokenizer(list(map(process_str, valid_acts)), max_length=512, truncation=True,  padding='max_length')\n    batch = {\n        'state_input_ids': state_encodings['input_ids'],\n        'state_attention_mask': state_encodings['attention_mask'],\n        'action_input_ids': action_encodings['input_ids'],\n        'action_attention_mask': action_encodings['attention_mask'],\n        'sizes': len(valid_acts),\n        'images': info['image_feat'].tolist(),\n        'labels': 0\n    }\n    batch = data_collator([batch])\n    outputs = bert_model(**batch)\n    if softmax:\n        idx = torch.multinomial(torch.nn.functional.softmax(outputs.logits[0], dim=0), 1)[0].item()\n    else:\n        idx = outputs.logits[0].argmax(0).item()\n    return valid_acts[idx]\n\ndef get_return_value(env, asin, options, search_terms, page_num, product):\n    asin_url = None\n\n    # Determine product URL + options based on environment\n    if env == 'webshop':\n        query_str = \"+\".join(search_terms.split())\n        options_str = json.dumps(options)\n        asin_url = (\n            f'{WEBSHOP_URL}/item_page/{WEBSHOP_SESSION}/'\n            f'{asin}/{query_str}/{page_num}/{options_str}'\n        )\n    else:\n        asin_url = f\"https://www.ebay.com/itm/{asin}\" if env == 'ebay' else \\\n            f\"https://www.amazon.com/dp/{asin}\"\n    \n    # Extract relevant fields for product\n    product_reduced = {k: v for k, v in product.items() if k in [\"asin\", \"Title\", \"Description\", \"BulletPoints\"]}\n    product_reduced[\"Description\"] = product_reduced[\"Description\"][:100] + \"...\"\n    product_reduced[\"Features\"] = product_reduced.pop(\"BulletPoints\")\n    product_reduced[\"Features\"] = product_reduced[\"Features\"][:100] + \"...\"\n\n    # Create HTML to show link to product\n    html = \"\"\"<!DOCTYPE html><html><head><title>Chosen Product</title></head><body>\"\"\"\n    html += f\"\"\"Product Image:<img src=\"{product[\"MainImage\"]}\" height=\"50px\" /><br>\"\"\" if len(product[\"MainImage\"]) > 0 else \"\"\n    html += f\"\"\"Link to Product:\n        <a href=\"{asin_url}\" style=\"color:blue;text-decoration:underline;\" target=\"_blank\">{asin_url}</a>\n        </body></html>\"\"\"\n\n    return product_reduced, options if len(options) > 0 else \"None Selected\", html\n        \n\ndef predict(obs, info):\n    \"\"\"\n    Given WebShop environment observation and info, predict an action.\n    \"\"\"\n    valid_acts = info['valid']\n    if valid_acts[0].startswith('click['):\n        return bert_predict(obs, info)\n    else:\n        return \"search[\" + bart_predict(process_goal(obs)) + \"]\"\n\ndef run_episode(goal, env, verbose=True):\n    \"\"\"\n    Interact with amazon to find a product given input goal.\n    Input: text goal\n    Output: a url of found item on amazon.\n    \"\"\"\n    env = env.lower()\n    if env not in ENVIRONMENTS:\n        print(f\"[ERROR] Environment {env} not recognized\")\n        \n    obs = \"Amazon Shopping Game\\nInstruction:\" + goal + \"\\n[button] search [button]\"\n    info = {'valid': ['search[stuff]'], 'image_feat': torch.zeros(512)}\n    product_map = {}\n    title_to_asin_map = {}\n    search_results_cache = {}\n    visited_asins, clicked_options = set(), set()\n    sub_page_type, page_type, page_num = None, None, None\n    search_terms, prod_title, asin = None, None, None\n    options = {}\n    \n    for i in range(100):\n        # Run prediction\n        action = predict(obs, info)\n        if verbose:\n            print(\"====\")\n            print(action)\n        \n        # Previous Page Type, Action -> Next Page Type\n        action_content = action[action.find(\"[\")+1:action.find(\"]\")]\n        prev_page_type = page_type\n        if action.startswith('search['):\n            page_type = Page.RESULTS\n            search_terms = action_content\n            page_num = 1\n        elif action.startswith('click['):\n            if action.startswith('click[item -'):\n                prod_title = action_content[len(\"item -\"):].strip()\n                found = False\n                for key in title_to_asin_map:\n                    if prod_title == key:\n                        asin = title_to_asin_map[key]\n                        page_type = Page.ITEM_PAGE\n                        visited_asins.add(asin)\n                        found = True\n                        break\n                if not found:\n                    raise Exception(\"Product to click not found\")\n                    \n            elif any(x.value in action for x in [Page.DESC, Page.FEATURES, Page.REVIEWS]):\n                page_type = Page.SUB_PAGE\n                sub_page_type = Page(action_content.lower())\n                \n            elif action == 'click[< prev]':\n                if sub_page_type is not None:\n                    page_type, sub_page_type = Page.ITEM_PAGE, None\n                elif prev_page_type == Page.ITEM_PAGE:\n                    page_type = Page.RESULTS\n                    options, clicked_options = {}, set()\n                elif prev_page_type == Page.RESULTS and page_num > 1:\n                    page_type = Page.RESULTS\n                    page_num -= 1\n                    \n            elif action == 'click[next >]':\n                page_type = Page.RESULTS\n                page_num += 1\n                \n            elif action.lower() == 'click[back to search]':\n                page_type = Page.SEARCH\n                \n            elif action == 'click[buy now]':\n                return get_return_value(env, asin, options, search_terms, page_num, product_map[asin])\n            \n            elif prev_page_type == Page.ITEM_PAGE:\n                found = False\n                for opt_name, opt_values in product_map[asin][\"options\"].items():\n                    if action_content in opt_values:\n                        options[opt_name] = action_content\n                        page_type = Page.ITEM_PAGE\n                        clicked_options.add(action_content)\n                        found = True\n                        break\n                if not found:\n                    raise Exception(\"Unrecognized action: \" + action)\n        else:\n            raise Exception(\"Unrecognized action:\" + action)\n        \n        if verbose:\n            print(f\"Parsing {page_type.value} page...\")\n        \n        # URL -> Real HTML -> Dict of Info\n        if page_type == Page.RESULTS:\n            if search_terms in search_results_cache:\n                data = search_results_cache[search_terms]\n                if verbose:\n                    print(f\"Loading cached results page for \\\"{search_terms}\\\"\")\n            else:\n                begin = time.time()\n                if env == 'amazon':\n                    data = parse_results_amz(search_terms, page_num, verbose)\n                if env == 'webshop':\n                    data = parse_results_ws(search_terms, page_num, verbose)\n                if env == 'ebay':\n                    data = parse_results_ebay(search_terms, page_num, verbose)\n                end = time.time()\n                if verbose:\n                    print(f\"Parsing search results took {end-begin} seconds\")\n\n                search_results_cache[search_terms] = data\n                for d in data:\n                    title_to_asin_map[d['Title']] = d['asin']\n        elif page_type == Page.ITEM_PAGE or page_type == Page.SUB_PAGE:\n            if asin in product_map:\n                if verbose:\n                    print(\"Loading cached item page for\", asin)\n                data = product_map[asin]\n            else:\n                begin = time.time()\n                if env == 'amazon':\n                    data = parse_item_page_amz(asin, verbose)\n                if env == 'webshop':\n                    data = parse_item_page_ws(asin, search_terms, page_num, options, verbose)\n                if env == 'ebay':\n                    data = parse_item_page_ebay(asin, verbose)\n                end = time.time()\n                if verbose:\n                    print(\"Parsing item page took\", end-begin, \"seconds\")\n                product_map[asin] = data\n        elif page_type == Page.SEARCH:\n            if verbose:\n                print(\"Executing search\")\n            obs = \"Amazon Shopping Game\\nInstruction:\" + goal + \"\\n[button] search [button]\"\n            info = {'valid': ['search[stuff]'], 'image_feat': torch.zeros(512)}\n            continue\n        else:\n            raise Exception(\"Page of type `\", page_type, \"` not found\")\n\n        # Dict of Info -> Fake HTML -> Text Observation\n        begin = time.time()\n        html_str = dict_to_fake_html(data, page_type, asin, sub_page_type, options, product_map, goal)\n        obs = convert_html_to_text(html_str, simple=False, clicked_options=clicked_options, visited_asins=visited_asins)\n        end = time.time()\n        if verbose:\n            print(\"[Page Info -> WebShop HTML -> Observation] took\", end-begin, \"seconds\")\n\n        # Dict of Info -> Valid Action State (Info)\n        begin = time.time()\n        prod_arg = product_map if page_type == Page.ITEM_PAGE else data\n        info = convert_dict_to_actions(page_type, prod_arg, asin, page_num)\n        end = time.time()\n        if verbose:\n            print(\"Extracting available actions took\", end-begin, \"seconds\")\n        \n        if i == 50:\n            return get_return_value(env, asin, options, search_terms, page_num, product_map[asin])\n\ngr.Interface(\n    fn=run_episode,\n    inputs=[\n        gr.inputs.Textbox(lines=7, label=\"Input Text\"),\n        gr.inputs.Radio(['Amazon', 'eBay'], type=\"value\", default=\"Amazon\", label='Environment')\n    ],\n    outputs=[\n        gr.outputs.JSON(label=\"Selected Product\"),\n        gr.outputs.JSON(label=\"Selected Options\"),\n        gr.outputs.HTML()\n    ],\n    examples=[\n        [\"I want to find a gold floor lamp with a glass shade and a nickel finish that i can use for my living room, and price lower than 270.00 dollars\", \"Amazon\"],\n        [\"I need some cute heart-shaped glittery cupcake picks as a gift to bring to a baby shower\", \"Amazon\"],\n        [\"I want to buy ballet shoes which have rubber sole in grey suede color and a size of 6\", \"Amazon\"],\n        [\"I would like a 7 piece king comforter set decorated with flowers and is machine washable\", \"Amazon\"],\n        [\"I'm trying to find white bluetooth speakers that are not only water resistant but also come with stereo sound\", \"eBay\"],\n        [\"find me the soy free 3.5 ounce 4-pack of dang thai rice chips, and make sure they are the aged cheddar flavor.  i also need the ones in the resealable bags\", \"eBay\"],\n        [\"I am looking for a milk chocolate of 1 pound size in a single pack for valentine day\", \"eBay\"],\n        [\"I'm looking for a mini pc intel core desktop computer which supports with windows 11\", \"eBay\"]\n    ],\n    title=\"WebShop\",\n    article=\"<p style='padding-top:15px;text-align:center;'>To learn more about this project, check out the <a href='https://webshop-pnlp.github.io/' target='_blank'>project page</a>!</p>\",\n    description=\"<p style='text-align:center;'>Sim-to-real transfer of agent trained on WebShop to search a desired product on Amazon from any natural language query!</p>\",\n).launch(inline=False)\n"
  },
  {
    "path": "envs/webshop/src/webshop/transfer/predict_help.py",
    "content": "from bs4 import BeautifulSoup\nfrom bs4.element import Comment\nfrom enum import Enum\nimport re, time\nfrom urllib.parse import urlencode\n\nimport json, requests, torch\n\nclass Page(Enum):\n    DESC = \"description\"\n    FEATURES = \"features\"\n    ITEM_PAGE = \"item_page\"\n    RESULTS = \"results\"\n    REVIEWS = \"reviews\"\n    SEARCH = \"search\"\n    SUB_PAGE = \"item_sub_page\"\n\nHEADER_ = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36'\nDEBUG_HTML = \"temp.html\"\nNUM_PROD_LIMIT = 10\n\nWEBSHOP_URL = \"http://3.83.245.205:3000\"\nWEBSHOP_SESSION = \"abc\"\n\n\ndef parse_results_ebay(query, page_num=None, verbose=True):\n    query_string = '+'.join(query.split())\n    page_num = 1 if page_num is None else page_num\n    url = f'https://www.ebay.com/sch/i.html?_nkw={query_string}&_pgn={page_num}'\n    if verbose:\n        print(f\"Search Results URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.text, 'html.parser')\n    products = soup.select('.s-item__wrapper.clearfix')\n\n    results = []\n    for item in products[:NUM_PROD_LIMIT]:\n        title = item.select_one('.s-item__title').text.strip()\n        if \"shop on ebay\" in title.lower():\n            # Skip \"Shop on ebay\" product title\n            continue\n        link = item.select_one('.s-item__link')['href']\n        asin = link.split(\"?\")[0][len(\"https://www.ebay.com/itm/\"):]\n\n        try:\n            price = item.select_one('.s-item__price').text\n            if \"to\" in price:\n                prices = price.split(\" to \")\n                price = [p.strip(\"$\") for p in prices]\n        except:\n            price = None\n        \n        results.append({\n            \"asin\": asin,\n            \"Title\": title,\n            \"Price\": price\n        })\n    if verbose:\n        print(f\"Scraped {len(results)} products\")\n    return results\n\n\ndef parse_item_page_ebay(asin, verbose=True):\n    product_dict = {}\n    product_dict[\"asin\"] = asin\n    \n    url = f\"https://www.ebay.com/itm/{asin}\"\n    if verbose:\n        print(f\"Item Page URL: {url}\")\n    begin = time.time()\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    end = time.time()\n    if verbose:\n        print(f\"Item page scraping took {end-begin} seconds\")\n    soup = BeautifulSoup(webpage.content, \"html.parser\")\n\n    # Title\n    try:\n        product_dict[\"Title\"] = soup.find('h1', {'class': 'x-item-title__mainTitle'}).text.strip()\n    except:\n        product_dict[\"Title\"] = \"N/A\"\n\n    # Price: Get price string, extract decimal numbers from string\n    try:\n        price_str = soup.find('div', {'class': 'mainPrice'}).text\n        prices = re.findall('\\d*\\.?\\d+', price_str)\n        product_dict[\"Price\"] = prices[0]\n    except:\n        product_dict[\"Price\"] = \"N/A\"\n\n     # Main Image\n    try:\n        img_div = soup.find('div', {'id': 'mainImgHldr'})\n        img_link = img_div.find('img', {'id': 'icImg'})[\"src\"]\n        product_dict[\"MainImage\"] = img_link\n    except:\n        product_dict[\"MainImage\"] = \"\"\n    \n    # Rating\n    try:\n        rating = soup.find('span', {'class': 'reviews-star-rating'})[\"title\"].split()[0]\n    except:\n        rating = None\n    product_dict[\"Rating\"] = rating\n\n    # Options\n    options, options_to_images = {}, {} # TODO: options_to_images possible?\n    try:\n        option_blocks = soup.findAll('select', {'class': 'msku-sel'})\n        for block in option_blocks:\n            name = block[\"name\"].strip().strip(\":\")\n            option_tags = block.findAll(\"option\")\n            opt_list = []\n            for option_tag in option_tags:\n                if \"select\" not in option_tag.text.lower():\n                    # Do not include \"- select -\" (aka `not selected`) choice\n                    opt_list.append(option_tag.text)\n            options[name] = opt_list\n    except:\n        options = {}\n    product_dict[\"options\"], product_dict[\"option_to_image\"] = options, options_to_images\n\n    # Description\n    desc = None\n    try:\n        # Ebay descriptions are shown in `iframe`s\n        desc_link = soup.find('iframe', {'id': 'desc_ifr'})[\"src\"]\n        desc_webpage = requests.get(desc_link, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n        desc_soup = BeautifulSoup(desc_webpage.content, \"html.parser\")\n        desc = ' '.join(desc_soup.text.split())\n    except:\n        desc = \"N/A\"\n    product_dict[\"Description\"] = desc\n\n    # Features\n    features = None\n    try:\n        features = soup.find('div', {'class': 'x-about-this-item'}).text\n    except:\n        features = \"N/A\"\n    product_dict[\"BulletPoints\"] = features\n\n    return product_dict\n    \n\ndef parse_results_ws(query, page_num=None, verbose=True):\n    query_string = '+'.join(query.split())\n    page_num = 1 if page_num is None else page_num\n    url = (\n        f'{WEBSHOP_URL}/search_results/{WEBSHOP_SESSION}/'\n        f'{query_string}/{page_num}'\n    )\n    if verbose:\n        print(f\"Search Results URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.content, 'html.parser')\n    products = soup.findAll('div', {'class': 'list-group-item'})\n\n    results = []\n    for product in products:\n        asin = product.find('a', {'class': 'product-link'})\n        title = product.find('h4', {'class': 'product-title'})\n        price = product.find('h5', {'class': 'product-price'})\n\n        if \"\\n\" in title:\n            title = title.text.split(\"\\n\")[0].strip()\n        else:\n            title = title.text.strip().strip(\"\\n\")\n\n        if \"to\" in price.text:\n            # Parse if price presented as range\n            prices = price.text.split(\" to \")\n            price = [float(p.strip().strip(\"\\n$\")) for p in prices]\n        else:\n            price = float(price.text.strip().strip(\"\\n$\"))\n\n        results.append({\n            \"asin\": asin.text,\n            \"Title\": title,\n            \"Price\": price\n        })\n\n    if verbose:\n        print(f\"Scraped {len(results)} products\")\n    return results\n\n\ndef parse_item_page_ws(asin, query, page_num, options, verbose=True):\n    product_dict = {}\n    product_dict[\"asin\"] = asin\n\n    query_string = '+'.join(query.split())\n    options_string = json.dumps(options)\n    url = (\n        f'{WEBSHOP_URL}/item_page/{WEBSHOP_SESSION}/'\n        f'{asin}/{query_string}/{page_num}/{options_string}'\n    )\n    if verbose:\n        print(f\"Item Page URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.content, 'html.parser')\n\n    # Title, Price, Rating, and MainImage\n    product_dict[\"Title\"] = soup.find('h2').text\n    \n    h4_headers = soup.findAll(\"h4\")\n    for header in h4_headers:\n        text = header.text\n        if \"Price\" in text:\n            product_dict[\"Price\"] = text.split(\":\")[1].strip().strip(\"$\")\n        elif \"Rating\" in text:\n            product_dict[\"Rating\"] = text.split(\":\")[1].strip()\n    \n    product_dict[\"MainImage\"] = soup.find('img')['src']\n\n    # Options\n    options, options_to_image = {}, {}\n    option_blocks = soup.findAll(\"div\", {'class': 'radio-toolbar'})\n    for block in option_blocks:\n        name = block.find(\"input\")[\"name\"]\n        labels = block.findAll(\"label\")\n        inputs = block.findAll(\"input\")\n        opt_list = []\n        for label, input in zip(labels, inputs):\n            opt = label.text\n            opt_img_path = input[\"onclick\"].split(\"href=\")[1].strip('\\';')\n            opt_img_url = f'{WEBSHOP_URL}{opt_img_path}'\n\n            opt_list.append(opt)\n            options_to_image[opt] = opt_img_url\n        options[name] = opt_list\n    product_dict[\"options\"] = options\n    product_dict[\"option_to_image\"] = options_to_image\n\n    # Description\n    url = (\n        f'{WEBSHOP_URL}/item_sub_page/{WEBSHOP_SESSION}/'\n        f'{asin}/{query_string}/{page_num}/Description/{options_string}'\n    )\n    if verbose:\n        print(f\"Item Description URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.content, 'html.parser')\n    product_dict[\"Description\"] = soup.find(name=\"p\", attrs={'class': 'product-info'}).text.strip()\n\n    # Features\n    url = (\n        f'{WEBSHOP_URL}/item_sub_page/{WEBSHOP_SESSION}/'\n        f'{asin}/{query_string}/{page_num}/Features/{options_string}'\n    )\n    if verbose:\n        print(f\"Item Features URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.content, 'html.parser')\n    bullets = soup.find(name=\"ul\").findAll(name=\"li\")\n    product_dict[\"BulletPoints\"] = '\\n'.join([b.text.strip() for b in bullets])\n\n    return product_dict\n\n\n# Query -> Search Result ASINs\ndef parse_results_amz(query, page_num=None, verbose=True):\n    url = 'https://www.amazon.com/s?k=' + query.replace(\" \", \"+\")\n    if page_num is not None:\n        url += \"&page=\" + str(page_num)\n    if verbose:\n        print(f\"Search Results URL: {url}\")\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    soup = BeautifulSoup(webpage.content, 'html.parser')\n    products = soup.findAll('div', {'data-component-type': 's-search-result'})\n    if products is None:\n        temp = open(DEBUG_HTML, \"w\")\n        temp.write(str(soup))\n        temp.close()\n        raise Exception(\"Couldn't find search results page, outputted html for inspection\")\n    results = []\n\n    for product in products[:NUM_PROD_LIMIT]:\n        asin = product['data-asin']\n        title = product.find(\"h2\", {'class': \"a-size-mini\"})\n        price_div = product.find(\"div\", {'class': 's-price-instructions-style'})\n        price = price_div.find(\"span\", {'class': 'a-offscreen'})\n\n        result = {\n            'asin': asin,\n            'Title': title.text.strip(),\n            'Price': price.text.strip().strip(\"$\")\n        }\n        results.append(result)\n    if verbose:\n        print(\"Scraped\", len(results), \"products\")\n    return results\n\n\n# Scrape information of each product\ndef parse_item_page_amz(asin, verbose=True):\n    product_dict = {}\n    product_dict[\"asin\"] = asin\n\n    url = f\"https://www.amazon.com/dp/{asin}\"\n    if verbose:\n        print(\"Item Page URL:\", url)\n    begin = time.time()\n    webpage = requests.get(url, headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})\n    end = time.time()\n    if verbose:\n        print(f\"Item page scraping took {end-begin} seconds\")\n    soup = BeautifulSoup(webpage.content, \"html.parser\")\n\n    # Title\n    try:\n        title = soup.find(\"span\", attrs={\"id\": 'productTitle'})\n        title = title.string.strip().replace(',', '')\n    except AttributeError:\n        title = \"N/A\"\n    product_dict[\"Title\"] = title\n \n    # Price\n    try:\n        parent_price_span = soup.find(name=\"span\", class_=\"apexPriceToPay\")\n        price_span = parent_price_span.find(name=\"span\", class_=\"a-offscreen\")\n        price = float(price_span.getText().replace(\"$\", \"\"))\n    except AttributeError:\n        price = \"N/A\"\n    product_dict[\"Price\"] = price\n\n    # Rating\n    try:\n        rating = soup.find(name=\"span\", attrs={\"id\": \"acrPopover\"})\n        if rating is None:\n            rating = \"N/A\"\n        else:\n            rating = rating.text\n    except AttributeError:\n        rating = \"N/A\"\n    product_dict[\"Rating\"] = rating.strip(\"\\n\").strip()\n \n    # Features\n    try:\n        features = soup.find(name=\"div\", attrs={\"id\": \"feature-bullets\"}).text\n    except AttributeError:\n        features = \"N/A\"\n    product_dict[\"BulletPoints\"] = features\n    \n    # Description\n    try:\n        desc_body = soup.find(name=\"div\", attrs={\"id\": \"productDescription_feature_div\"})\n        desc_div = desc_body.find(name=\"div\", attrs={\"id\": \"productDescription\"})\n        desc_ps = desc_div.findAll(name=\"p\")\n        desc = \" \".join([p.text for p in desc_ps])\n    except AttributeError:\n        desc = \"N/A\"\n    product_dict[\"Description\"] = desc.strip()\n\n    # Main Image\n    try:\n        imgtag = soup.find(\"img\", {\"id\":\"landingImage\"})\n        imageurl = dict(imgtag.attrs)[\"src\"]\n    except AttributeError:\n        imageurl = \"\"\n    product_dict[\"MainImage\"] = imageurl\n\n    # Options\n    options, options_to_image = {}, {}\n    try:\n        option_body = soup.find(name='div', attrs={\"id\": \"softlinesTwister_feature_div\"})\n        if option_body is None:\n            option_body = soup.find(name='div', attrs={\"id\": \"twister_feature_div\"})\n        option_blocks = option_body.findAll(name='ul')\n        for block in option_blocks:\n            name = json.loads(block[\"data-a-button-group\"])[\"name\"]\n            # Options\n            opt_list = []\n            for li in block.findAll(\"li\"):\n                img = li.find(name=\"img\")\n                if img is not None:\n                    opt = img[\"alt\"].strip()\n                    opt_img = img[\"src\"]\n                    if len(opt) > 0:\n                        options_to_image[opt] = opt_img\n                else:\n                    opt = li.text.strip()\n                if len(opt) > 0:\n                    opt_list.append(opt)\n            options[name.replace(\"_name\", \"\").replace(\"twister_\", \"\")] = opt_list\n    except AttributeError:\n        options = {}\n    product_dict[\"options\"], product_dict[\"option_to_image\"] = options, options_to_image\n    return product_dict\n\n\n# Get text observation from html\n# TODO[john-b-yang]: Similar to web_agent_site/envs/...text_env.py func def, merge?\ndef convert_html_to_text(html, simple=False, clicked_options=None, visited_asins=None):\n    def tag_visible(element):\n        ignore = {'style', 'script', 'head', 'title', 'meta', '[document]'}\n        return (\n            element.parent.name not in ignore and not isinstance(element, Comment)\n        )\n    html_obj = BeautifulSoup(html, 'html.parser')\n    texts = html_obj.find_all(string=True)\n    visible_texts = filter(tag_visible, texts)\n    if simple:\n        return ' [SEP] '.join(t.strip() for t in visible_texts if t != '\\n')\n    else:\n        observation = ''\n        for t in visible_texts:\n            if t == '\\n': continue\n            if t.parent.name == 'button':  # button\n                processed_t = f'[button] {t} [button]'\n            elif t.parent.name == 'label':  # options\n                if f'{t}' in clicked_options:\n                    processed_t = f'  [clicked button] {t} [clicked button]'\n                    observation = f'You have clicked {t}.\\n' + observation\n                else:\n                    processed_t = f'  [button] {t} [button]'\n            elif t.parent.get('class') == [\"product-link\"]: # asins\n                if f'{t}' in visited_asins:\n                    processed_t = f'\\n[clicked button] {t} [clicked button]'\n                else:\n                    processed_t = f'\\n[button] {t} [button]'\n            else: # regular, unclickable text\n                processed_t =  str(t)\n            observation += processed_t + '\\n'\n        return observation\n\n\n# Get action from dict of values retrieved from html\ndef convert_dict_to_actions(page_type, products=None, asin=None, page_num=None) -> dict:\n    info = {\"valid\": []}\n    if page_type == Page.RESULTS:\n        info[\"valid\"] = ['click[back to search]']\n        if products is None or page_num is None:\n            print(page_num)\n            print(products)\n            raise Exception('Provide `products`, `page_num` to get `results` valid actions')\n        # Decide whether to add `next >` as clickable based on # of search results\n        if len(products) > 10:\n            info[\"valid\"].append('click[next >]')\n        # Add `< prev` as clickable if not first page of search results\n        if page_num > 1:\n            info[\"valid\"].append('click[< prev]')\n        for product in products:\n            info[\"valid\"].append(\"click[item - \" + product[\"Title\"] + \"]\")\n    if page_type == Page.ITEM_PAGE:\n        if products is None or asin is None:\n            raise Exception('Provide `products` and `asin` to get `item_page` valid actions')\n        info[\"valid\"] = ['click[back to search]', 'click[< prev]', 'click[description]',\\\n            'click[features]', 'click[buy now]'] # To do: reviews\n        if \"options\" in products[asin]:\n            for key, values in products[asin][\"options\"].items():\n                for value in values:\n                    info[\"valid\"].append(\"click[\" + value + \"]\")\n    if page_type == Page.SUB_PAGE:\n        info[\"valid\"] = ['click[back to search]', 'click[< prev]']\n    info['image_feat'] = torch.zeros(512)\n    return info"
  },
  {
    "path": "envs/webshop/src/webshop/transfer/webshop_lite.py",
    "content": "import os\n\nfrom flask import render_template_string, Flask\nfrom .predict_help import Page\n\napp=Flask(__name__)\napp.debug=True\n\nSESSION_ID = \"ABC\"\nTEMPLATE_DIR = \"envs/webshop/src/webshop/web_agent_site/templates/\"\nKEYWORDS = [\"placeholder (not needed)\"] # To Do: Does this matter?\nQUERY = \"\"\nproduct_map = {}\n\ndef read_html_template(path):\n    with open(path) as f:\n        template = f.read()\n    return template\n\n@app.route('/', methods=['GET', 'POST'])\ndef index(session_id, **kwargs):\n    print(\"Hello world\")\n\n@app.route('/', methods=['GET', 'POST'])\ndef search_results(data):\n    path = os.path.join(TEMPLATE_DIR, 'results_page.html')\n    html = render_template_string(\n        read_html_template(path=path),\n        session_id=SESSION_ID,\n        products=data,\n        keywords=KEYWORDS,\n        page=1,\n        total=len(data),\n        instruction_text=QUERY,\n    )\n    return html\n\n@app.route('/', methods=['GET', 'POST'])\ndef item_page(session_id, asin, keywords, page, options):\n    path = os.path.join(TEMPLATE_DIR, 'item_page.html')\n    html = render_template_string(\n        read_html_template(path=path),\n        session_id=session_id,\n        product_info=product_map[asin],\n        keywords=keywords,\n        page=page,\n        asin=asin,\n        options=options,\n        instruction_text=QUERY\n    )\n    return html\n\n@app.route('/', methods=['GET', 'POST'])\ndef item_sub_page(session_id, asin, keywords, page, sub_page, options):\n    path = os.path.join(TEMPLATE_DIR, sub_page.value.lower() + \"_page.html\")\n    html = render_template_string(\n        read_html_template(path),\n        session_id=session_id, \n        product_info=product_map[asin],\n        keywords=keywords,\n        page=page,\n        asin=asin,\n        options=options,\n        instruction_text=QUERY\n    )\n    return html\n\n@app.route('/', methods=['GET', 'POST'])\ndef done(asin, options, session_id, **kwargs):\n    path = os.path.join(TEMPLATE_DIR, 'done_page.html')\n    html = render_template_string(\n        read_html_template(path),\n        session_id=session_id,\n        reward=1,\n        asin=asin,\n        options=product_map[asin][\"options\"],\n        reward_info=kwargs.get('reward_info'),\n        goal_attrs=kwargs.get('goal_attrs'),\n        purchased_attrs=kwargs.get('purchased_attrs'),\n        goal=kwargs.get('goal'),\n        mturk_code=kwargs.get('mturk_code'),\n        query=kwargs.get('query'),\n        category=kwargs.get('category'),\n        product_category=kwargs.get('product_category'),\n    )\n    return html\n    \n# Project Dictionary Information onto Fake Amazon\ndef dict_to_fake_html(data, page_type, asin=None, sub_page_type=None, options=None, prod_map={}, query=\"\"):\n    global QUERY, product_map\n    QUERY = query\n    product_map = prod_map\n    with app.app_context(), app.test_request_context():\n        if page_type == Page.RESULTS:\n            return search_results(data)\n        if page_type == Page.ITEM_PAGE:\n            return item_page(SESSION_ID, asin, KEYWORDS, 1, options)\n        if page_type == Page.SUB_PAGE:\n            if sub_page_type is not None:\n                return item_sub_page(SESSION_ID, asin, KEYWORDS, 1, sub_page_type, options)\n            else:\n                raise Exception(\"Sub page of type\", sub_page_type, \"unrecognized\")"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/__init__.py",
    "content": ""
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/app.py",
    "content": "import argparse, json, logging, random\nfrom pathlib import Path\nfrom ast import literal_eval\n\nfrom flask import (\n    Flask,\n    request,\n    redirect,\n    url_for\n)\n\nfrom rich import print\n\nfrom .engine.engine import (\n    load_products,\n    init_search_engine,\n    convert_web_app_string_to_var,\n    get_top_n_product_from_keywords,\n    get_product_per_page,\n    map_action_to_html,\n    END_BUTTON\n)\nfrom .engine.goal import get_reward, get_goals\nfrom .utils import (\n    generate_mturk_code,\n    setup_logger,\n    DEFAULT_FILE_PATH,\n    DEBUG_PROD_SIZE,\n)\n\napp = Flask(__name__)\n\nsearch_engine = None\nall_products = None\nproduct_item_dict = None\nproduct_prices = None\nattribute_to_asins = None\ngoals = None\nweights = None\n\nuser_sessions = dict()\nuser_log_dir = None\nSHOW_ATTRS_TAB = False\n\n@app.route('/')\ndef home():\n    return redirect(url_for('index', session_id=\"abc\"))\n\n@app.route('/<session_id>', methods=['GET', 'POST'])\ndef index(session_id):\n    global user_log_dir\n    global all_products, product_item_dict, \\\n           product_prices, attribute_to_asins, \\\n           search_engine, \\\n           goals, weights, user_sessions\n\n    if search_engine is None:\n        all_products, product_item_dict, product_prices, attribute_to_asins = \\\n            load_products(\n                filepath=DEFAULT_FILE_PATH,\n                num_products=DEBUG_PROD_SIZE\n            )\n        search_engine = init_search_engine(num_products=DEBUG_PROD_SIZE)\n        goals = get_goals(all_products, product_prices)\n        random.seed(233)\n        random.shuffle(goals)\n        weights = [goal['weight'] for goal in goals]\n\n    if session_id not in user_sessions and 'fixed' in session_id:\n        goal_dix = int(session_id.split('_')[-1])\n        goal = goals[goal_dix]\n        instruction_text = goal['instruction_text']\n        user_sessions[session_id] = {'goal': goal, 'done': False}\n        if user_log_dir is not None:\n            setup_logger(session_id, user_log_dir)\n    elif session_id not in user_sessions:\n        goal = random.choices(goals, weights)[0]\n        instruction_text = goal['instruction_text']\n        user_sessions[session_id] = {'goal': goal, 'done': False}\n        if user_log_dir is not None:\n            setup_logger(session_id, user_log_dir)\n    else:\n        instruction_text = user_sessions[session_id]['goal']['instruction_text']\n\n    if request.method == 'POST' and 'search_query' in request.form:\n        keywords = request.form['search_query'].lower().split(' ')\n        return redirect(url_for(\n            'search_results',\n            session_id=session_id,\n            keywords=keywords,\n            page=1,\n        ))\n    if user_log_dir is not None:\n        logger = logging.getLogger(session_id)\n        logger.info(json.dumps(dict(\n            page='index',\n            url=request.url,\n            goal=user_sessions[session_id]['goal'],\n        )))\n    return map_action_to_html(\n        'start',\n        session_id=session_id,\n        instruction_text=instruction_text,\n    )\n\n\n@app.route(\n    '/search_results/<session_id>/<keywords>/<page>',\n    methods=['GET', 'POST']\n)\ndef search_results(session_id, keywords, page):\n    instruction_text = user_sessions[session_id]['goal']['instruction_text']\n    page = convert_web_app_string_to_var('page', page)\n    keywords = convert_web_app_string_to_var('keywords', keywords)\n    top_n_products = get_top_n_product_from_keywords(\n        keywords,\n        search_engine,\n        all_products,\n        product_item_dict,\n        attribute_to_asins,\n    )\n    products = get_product_per_page(top_n_products, page)\n    html = map_action_to_html(\n        'search',\n        session_id=session_id,\n        products=products,\n        keywords=keywords,\n        page=page,\n        total=len(top_n_products),\n        instruction_text=instruction_text,\n    )\n    logger = logging.getLogger(session_id)\n    logger.info(json.dumps(dict(\n        page='search_results',\n        url=request.url,\n        goal=user_sessions[session_id]['goal'],\n        content=dict(\n            keywords=keywords,\n            search_result_asins=[p['asin'] for p in products],\n            page=page,\n        )\n    )))\n    return html\n\n\n@app.route(\n    '/item_page/<session_id>/<asin>/<keywords>/<page>/<options>',\n    methods=['GET', 'POST']\n)\ndef item_page(session_id, asin, keywords, page, options):\n    options = literal_eval(options)\n    product_info = product_item_dict[asin]\n\n    goal_instruction = user_sessions[session_id]['goal']['instruction_text']\n    product_info['goal_instruction'] = goal_instruction\n\n    html = map_action_to_html(\n        'click',\n        session_id=session_id,\n        product_info=product_info,\n        keywords=keywords,\n        page=page,\n        asin=asin,\n        options=options,\n        instruction_text=goal_instruction,\n        show_attrs=SHOW_ATTRS_TAB,\n    )\n    logger = logging.getLogger(session_id)\n    logger.info(json.dumps(dict(\n        page='item_page',\n        url=request.url,\n        goal=user_sessions[session_id]['goal'],\n        content=dict(\n            keywords=keywords,\n            page=page,\n            asin=asin,\n            options=options,\n        )\n    )))\n    return html\n\n\n@app.route(\n    '/item_sub_page/<session_id>/<asin>/<keywords>/<page>/<sub_page>/<options>',\n    methods=['GET', 'POST']\n)\ndef item_sub_page(session_id, asin, keywords, page, sub_page, options):\n    options = literal_eval(options)\n    product_info = product_item_dict[asin]\n\n    goal_instruction = user_sessions[session_id]['goal']['instruction_text']\n    product_info['goal_instruction'] = goal_instruction\n\n    html = map_action_to_html(\n        f'click[{sub_page}]',\n        session_id=session_id,\n        product_info=product_info,\n        keywords=keywords,\n        page=page,\n        asin=asin,\n        options=options,\n        instruction_text=goal_instruction\n    )\n    logger = logging.getLogger(session_id)\n    logger.info(json.dumps(dict(\n        page='item_sub_page',\n        url=request.url,\n        goal=user_sessions[session_id]['goal'],\n        content=dict(\n            keywords=keywords,\n            page=page,\n            asin=asin,\n            options=options,\n        )\n    )))\n    return html\n\n\n@app.route('/done/<session_id>/<asin>/<options>', methods=['GET', 'POST'])\ndef done(session_id, asin, options):\n    options = literal_eval(options)\n    goal = user_sessions[session_id]['goal']\n    purchased_product = product_item_dict[asin]\n    price = product_prices[asin]\n\n    reward, reward_info = get_reward(\n        purchased_product,\n        goal,\n        price=price,\n        options=options,\n        verbose=True\n    )\n    user_sessions[session_id]['done'] = True\n    user_sessions[session_id]['reward'] = reward\n    print(user_sessions)\n\n    logger = logging.getLogger(session_id)\n    logger.info(json.dumps(dict(\n        page='done',\n        url=request.url,\n        goal=goal,\n        content=dict(\n            asin=asin,\n            options=options,\n            price=price,\n        ),\n        reward=reward,\n        reward_info=reward_info,\n    )))\n    del logging.root.manager.loggerDict[session_id]\n    \n    return map_action_to_html(\n        f'click[{END_BUTTON}]',\n        session_id=session_id,\n        reward=reward,\n        asin=asin,\n        options=options,\n        reward_info=reward_info,\n        query=purchased_product['query'],\n        category=purchased_product['category'],\n        product_category=purchased_product['product_category'],\n        goal_attrs=user_sessions[session_id]['goal']['attributes'],\n        purchased_attrs=purchased_product['Attributes'],\n        goal=goal,\n        mturk_code=generate_mturk_code(session_id),\n    )\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"WebShop flask app backend configuration\")\n    parser.add_argument(\"--log\", action='store_true', help=\"Log actions on WebShop in trajectory file\")\n    parser.add_argument(\"--attrs\", action='store_true', help=\"Show attributes tab in item page\")\n    parser.add_argument(\"--port\", type=int, default=3000, help=\"Port to run the app on\")\n\n    args = parser.parse_args()\n    if args.log:\n        user_log_dir = Path('user_session_logs/mturk')\n        user_log_dir.mkdir(parents=True, exist_ok=True)\n    SHOW_ATTRS_TAB = args.attrs\n\n    app.run(host='0.0.0.0', port=args.port)\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/attributes/annotate.py",
    "content": "import yaml\nfrom pathlib import Path\nfrom rich import print\n\nATTR_DIR = './data/attributes'\n\nATTR_PATHS = [\n    'narrow_2-gram.yaml',\n    'narrow_1-gram.yaml',\n    'broad_2-gram.yaml',\n    'broad_1-gram.yaml',\n]\nATTR_PATHS = [Path(ATTR_DIR) / af for af in ATTR_PATHS]\n\n\ndef annotate(attr_path):\n    with open(attr_path) as f:\n        attrs_by_cat = yaml.safe_load(f)\n\n    unique_attrs = set()\n    all_attrs = []\n    for _, attrs in attrs_by_cat.items():\n        attrs = [a.split('|')[0].strip() for a in attrs]\n        unique_attrs.update(attrs)\n        all_attrs += attrs\n    print(f'Total unique attributes: {len(unique_attrs)}')\n    total = len(all_attrs)\n    num_left = len(all_attrs)\n\n    annotated_attrs_by_cat = dict()\n    for category, attrs in attrs_by_cat.items():\n        print(\n            f'Category: [ {category} ] | '\n            f'Number of attributes: {len(attrs)}\\n'\n        )\n        annotated_attrs = []\n        for i, attr in enumerate(attrs):\n            attr, score = attr.split(' | ')\n            print(\n                f'{\"[\" + str(i) + \"]\":<5} '\n                f'[bold green]{attr:<30}[/bold green] | '\n                f'[red]{category}[/red] | '\n                f'{score}'\n            )\n            tags = input(\n                'Annotate [1: ITEM, 2: PROP, 3: USE, '\n                '⎵: next example, q: next category] > '\n            )\n            print('\\n')\n            tags = tags.strip()\n            annotated_attrs.append(f'{attr} | {score} | {tags}')\n            if 'q' in tags:\n                break\n        \n        num_left -= len(attrs)\n        print(f'{num_left} / {total} total attributes left.')\n\n        ans = input('Starting the next category... [y/n] > ')\n        if ans == 'n':\n            break\n\ndef main():\n    for attr_path in ATTR_PATHS:\n        annotate(attr_path)\n\nif __name__ == '__main__':\n    \"\"\"\n    python -m web_agent_site.attributes.annotate\n    \"\"\"\n    main()\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/attributes/generate_attrs.py",
    "content": "import json\nimport yaml\nimport random\nfrom pathlib import Path\nfrom collections import defaultdict\n\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.feature_extraction import text as sk_text\nimport pandas as pd\nfrom tqdm import tqdm\nfrom rich import print\n\nITEMS_PATH = './data/ITEMS_mar1.json'\nREVIEWS_PATH = './data/reviews.json'\nATTR_DIR = './data/attributes'\n\nrandom.seed(0)\n\n\ndef get_stop_words():\n    extra_stop_words = set([str(i) for i in range(1000)])\n    stop_words = sk_text.ENGLISH_STOP_WORDS.union(extra_stop_words)\n    return stop_words\n\n\ndef load_products(num=None):\n    \"\"\"\n    Loads products from the `items.json` file and combine them with reviews\n    through `asin`.\n    Return: dict[asin, product]\n    \"\"\"\n    with open(ITEMS_PATH) as f:\n        all_products = json.load(f)\n        if num is not None:\n            random.shuffle(all_products)\n            all_products = all_products[:num]\n        products = dict()\n        asins = set()\n        for p in all_products:\n            asin = p['asin']\n            if asin in asins:\n                continue\n            asins.add(asin)\n            products[asin] = p\n\n    with open(REVIEWS_PATH) as f:\n        reviews = json.load(f)\n        reviews = {r['asin']: r for r in reviews}\n\n    for asin, p in products.items():\n        if asin in reviews:\n            p['review'] = reviews[asin]\n        else:\n            p['review'] = None\n    return products\n\n\ndef get_top_attrs(attributes, k):\n    attr_to_asins = defaultdict(list)\n\n    for asin, attr_scores in attributes.items():\n        top_attr_scoress = attr_scores[:k]\n        for attr, score in top_attr_scoress:\n            attr_to_asins[attr].append(asin)\n    total = len([asin for asin, _ in attributes.items()])\n    \n    top_attrs = [\n        (attr, len(asins) / total)\n        for attr, asins in attr_to_asins.items()\n    ]\n    top_attrs = sorted(top_attrs, key=lambda x: -x[1])\n    top_attrs = [f'{attr} | {score:.4f}' for attr, score in top_attrs]\n    return top_attrs\n\n\ndef get_corpus(\n        products,\n        keys=('name', 'small_description'),\n        category_type='category'\n    ):\n    \"\"\"\n    keys: `name`, `small_description`, `review`\n    category_type: `category`, `query`\n    \"\"\"\n    all_products = list(products.values())\n    \n    asins_by_cat = defaultdict(set)\n    corpus_by_cat = defaultdict(list)\n    for p in all_products:\n        category = p[category_type]\n        asin = p['asin']\n        if asin in asins_by_cat[category]:\n            continue\n        asins_by_cat[category].add(asin)\n\n        text = []\n        for key in keys:\n            if key == 'review':\n                rs = p['review']['reviews']\n                if r is not None:\n                    text_ = ' '.join([r['review'].lower() for r in rs])\n                else:\n                    text_ = ''\n            else:\n                text_ = p[key].lower()\n            text.append(text_)\n        text = ' '.join(text)\n        corpus_by_cat[category].append((asin, text))\n    return corpus_by_cat\n\n\ndef generate_ngram_attrs(corpus_by_cat, ngram_range, k, attrs):\n    vectorizer = TfidfVectorizer(\n        stop_words=get_stop_words(),\n        ngram_range=ngram_range,\n        max_features=1000,\n    )\n\n    top_attrs_by_cat = dict()\n    for category, corpus in tqdm(corpus_by_cat.items(),\n                                 total=len(corpus_by_cat)):\n        asins = [_[0] for _ in corpus]\n        texts = [_[1] for _ in corpus]\n        vec = vectorizer.fit_transform(texts).todense()\n        df = pd.DataFrame(vec, columns=vectorizer.get_feature_names_out())\n\n        attrs_by_cat = dict()\n        for asin, (row_name, row) in zip(asins, df.iterrows()):\n            attr_scores = sorted(\n                list(zip(row.index, row)),\n                key=lambda x: -x[1]\n            )\n            attrs_by_cat[asin] = attr_scores\n            attrs[asin] = attr_scores\n        top_attrs_by_cat[category.lower()] = get_top_attrs(attrs_by_cat, k=k)\n    print(top_attrs_by_cat.keys())\n    return top_attrs_by_cat\n\n\ndef generate_attrs(corpus_by_cat, k, save_name):\n    attrs = dict()\n    for n in range(1, 3):\n        ngram_range = (n, n)\n        top_attrs_by_cat = \\\n            generate_ngram_attrs(corpus_by_cat, ngram_range, k, attrs)\n\n        if save_name is not None:\n            save_path = Path(ATTR_DIR) / f'{save_name}_{n}-gram.yaml'\n            with open(save_path, 'w') as f:\n                yaml.dump(top_attrs_by_cat, f, default_flow_style=False)\n            print(f'Saved: {save_path}')\n\n    save_path = Path(ATTR_DIR) / f'{save_name}_attrs_unfiltered.json'\n    with open(save_path, 'w') as f:\n        json.dump(attrs, f)\n    print(f'Saved: {save_path}')\n\n\nif __name__ == '__main__':\n    \"\"\"\n    python -m web_agent_site.attributes.generate_attrs\n\n    Inspect in notebooks/attributes.ipynb.\n    \"\"\"\n    products = load_products(num=40000)\n\n    corpus_by_cat_broad = get_corpus(products, category_type='category')\n    generate_attrs(corpus_by_cat_broad, k=5, save_name='broad')\n\n    corpus_by_cat_narrow = get_corpus(products, category_type='query')\n    generate_attrs(corpus_by_cat_narrow, k=5, save_name='narrow')\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/engine/__init__.py",
    "content": ""
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/engine/engine.py",
    "content": "\"\"\"\n\"\"\"\nimport os\nimport re\nimport json\nimport random\nfrom collections import defaultdict\nfrom ast import literal_eval\nfrom decimal import Decimal\n\nimport cleantext\nfrom tqdm import tqdm\nfrom rank_bm25 import BM25Okapi\nfrom flask import render_template_string\nfrom rich import print\nfrom pyserini.search.lucene import LuceneSearcher\n\nfrom ..utils import (\n    BASE_DIR,\n    DEFAULT_FILE_PATH,\n    DEFAULT_REVIEW_PATH,\n    DEFAULT_ATTR_PATH,\n    HUMAN_ATTR_PATH\n)\n\nTEMPLATE_DIR = os.path.join(BASE_DIR, 'webshop/web_agent_site/templates')\n\nSEARCH_RETURN_N = 50\nPRODUCT_WINDOW = 10\nTOP_K_ATTR = 10\n\nEND_BUTTON = 'Buy Now'\nNEXT_PAGE = 'Next >'\nPREV_PAGE = '< Prev'\nBACK_TO_SEARCH = 'Back to Search'\n\nACTION_TO_TEMPLATE = {\n    'Description': 'description_page.html',\n    'Features': 'features_page.html',\n    'Reviews': 'review_page.html',\n    'Attributes': 'attributes_page.html',\n}\n\ndef map_action_to_html(action, **kwargs):\n    action_name, action_arg = parse_action(action)\n    if action_name == 'start':\n        path = os.path.join(TEMPLATE_DIR, 'search_page.html')\n        html = render_template_string(\n            read_html_template(path=path),\n            session_id=kwargs['session_id'],\n            instruction_text=kwargs['instruction_text'],\n        )\n    elif action_name == 'search':\n        path = os.path.join(TEMPLATE_DIR, 'results_page.html')\n        html = render_template_string(\n            read_html_template(path=path),\n            session_id=kwargs['session_id'],\n            products=kwargs['products'],\n            keywords=kwargs['keywords'],\n            page=kwargs['page'],\n            total=kwargs['total'],\n            instruction_text=kwargs['instruction_text'],\n        )\n    elif action_name == 'click' and action_arg == END_BUTTON:\n        path = os.path.join(TEMPLATE_DIR, 'done_page.html')\n        html = render_template_string(\n            read_html_template(path),\n            session_id=kwargs['session_id'],\n            reward=kwargs['reward'],\n            asin=kwargs['asin'],\n            options=kwargs['options'],\n            reward_info=kwargs.get('reward_info'),\n            goal_attrs=kwargs.get('goal_attrs'),\n            purchased_attrs=kwargs.get('purchased_attrs'),\n            goal=kwargs.get('goal'),\n            mturk_code=kwargs.get('mturk_code'),\n            query=kwargs.get('query'),\n            category=kwargs.get('category'),\n            product_category=kwargs.get('product_category'),\n        )\n    elif action_name == 'click' and action_arg in ACTION_TO_TEMPLATE:\n        path = os.path.join(TEMPLATE_DIR, ACTION_TO_TEMPLATE[action_arg])\n        html = render_template_string(\n            read_html_template(path),\n            session_id=kwargs['session_id'],\n            product_info=kwargs['product_info'],\n            keywords=kwargs['keywords'],\n            page=kwargs['page'],\n            asin=kwargs['asin'],\n            options=kwargs['options'],\n            instruction_text=kwargs.get('instruction_text')\n        )\n    elif action_name == 'click':\n        path = os.path.join(TEMPLATE_DIR, 'item_page.html')\n        html = render_template_string(\n            read_html_template(path),\n            session_id=kwargs['session_id'],\n            product_info=kwargs['product_info'],\n            keywords=kwargs['keywords'],\n            page=kwargs['page'],\n            asin=kwargs['asin'],\n            options=kwargs['options'],\n            instruction_text=kwargs.get('instruction_text'),\n            show_attrs=kwargs['show_attrs']\n        )\n    else:\n        raise ValueError('Action name not recognized.')\n    return html\n\n\ndef read_html_template(path):\n    with open(path) as f:\n        template = f.read()\n    return template\n\n\ndef parse_action(action):\n    \"\"\"\n    Parse action string to action name and its arguments.\n    \"\"\"\n    pattern = re.compile(r'(.+)\\[(.+)\\]')\n    m = re.match(pattern, action)\n    if m is None:\n        action_name = action\n        action_arg = None\n    else:\n        action_name, action_arg = m.groups()\n    return action_name, action_arg\n\n\ndef convert_web_app_string_to_var(name, string):\n    if name == 'keywords':\n        keywords = string\n        if keywords.startswith('['):\n            keywords = literal_eval(keywords)\n        else:\n            keywords = [keywords]\n        var = keywords\n    elif name == 'page':\n        page = string\n        page = int(page)\n        var = page\n    else:\n        raise ValueError('Name of variable not recognized.')\n    return var\n\n\ndef get_top_n_product_from_keywords(\n        keywords,\n        search_engine,\n        all_products,\n        product_item_dict,\n        attribute_to_asins=None,\n    ):\n    if keywords[0] == '<r>':\n        top_n_products = random.sample(all_products, k=SEARCH_RETURN_N)\n    elif keywords[0] == '<a>':\n        attribute = ' '.join(keywords[1:]).strip()\n        asins = attribute_to_asins[attribute]\n        top_n_products = [p for p in all_products if p['asin'] in asins]\n    elif keywords[0] == '<c>':\n        category = keywords[1].strip()\n        top_n_products = [p for p in all_products if p['category'] == category]\n    elif keywords[0] == '<q>':\n        query = ' '.join(keywords[1:]).strip()\n        top_n_products = [p for p in all_products if p['query'] == query]\n    else:\n        keywords = ' '.join(keywords)\n        hits = search_engine.search(keywords, k=SEARCH_RETURN_N)\n        docs = [search_engine.doc(hit.docid) for hit in hits]\n        top_n_asins = [json.loads(doc.raw())['id'] for doc in docs]\n        top_n_products = [product_item_dict[asin] for asin in top_n_asins if asin in product_item_dict]\n    return top_n_products\n\n\ndef get_product_per_page(top_n_products, page):\n    return top_n_products[(page - 1) * PRODUCT_WINDOW:page * PRODUCT_WINDOW]\n\n\ndef generate_product_prices(all_products):\n    product_prices = dict()\n    for product in all_products:\n        asin = product['asin']\n        pricing = product['pricing']\n        if not pricing:\n            price = 100.0\n        elif len(pricing) == 1:\n            price = pricing[0]\n        else:\n            price = random.uniform(*pricing[:2])\n        product_prices[asin] = price\n    return product_prices\n\n\ndef init_search_engine(num_products=None):\n    if num_products == 100:\n        indexes = 'indexes_100'\n    elif num_products == 1000:\n        indexes = 'indexes_1k'\n    elif num_products == 100000:\n        indexes = 'indexes_100k'\n    elif num_products is None:\n        indexes = 'indexes'\n    else:\n        raise NotImplementedError(f'num_products being {num_products} is not supported yet.')\n    index_dir = os.path.abspath(os.path.join(BASE_DIR, f'../search_index/{indexes}'))\n    assert os.path.isdir(index_dir), f'Index dir missing: {index_dir}'\n    search_engine = LuceneSearcher(index_dir)\n    # search_engine = LuceneSearcher(os.path.join(BASE_DIR, f'../search_index/indexes'))\n    return search_engine\n\n\ndef clean_product_keys(products, quiet: bool = False):\n    for product in products:\n        product.pop('product_information', None)\n        product.pop('brand', None)\n        product.pop('brand_url', None)\n        product.pop('list_price', None)\n        product.pop('availability_quantity', None)\n        product.pop('availability_status', None)\n        product.pop('total_reviews', None)\n        product.pop('total_answered_questions', None)\n        product.pop('seller_id', None)\n        product.pop('seller_name', None)\n        product.pop('fulfilled_by_amazon', None)\n        product.pop('fast_track_message', None)\n        product.pop('aplus_present', None)\n        product.pop('small_description_old', None)\n    if not quiet:\n        print('Keys cleaned.')\n    return products\n\n\ndef load_products(filepath, num_products=None, human_goals=True, quiet: bool = False):\n    # TODO: move to preprocessing step -> enforce single source of truth\n    with open(filepath) as f:\n        products = json.load(f)\n    if not quiet:\n        print('Products loaded.')\n    products = clean_product_keys(products, quiet=quiet)\n    \n    # with open(DEFAULT_REVIEW_PATH) as f:\n    #     reviews = json.load(f)\n    all_reviews = dict()\n    all_ratings = dict()\n    # for r in reviews:\n    #     all_reviews[r['asin']] = r['reviews']\n    #     all_ratings[r['asin']] = r['average_rating']\n\n    if human_goals:\n        with open(HUMAN_ATTR_PATH) as f:\n            human_attributes = json.load(f)\n    with open(DEFAULT_ATTR_PATH) as f:\n        attributes = json.load(f)\n    with open(HUMAN_ATTR_PATH) as f:\n        human_attributes = json.load(f)\n    if not quiet:\n        print('Attributes loaded.')\n\n    asins = set()\n    all_products = []\n    attribute_to_asins = defaultdict(set)\n    if num_products is not None:\n        # using item_shuffle.json, we assume products already shuffled\n        products = products[:num_products]\n    for i, p in tqdm(enumerate(products), total=len(products), disable=quiet):\n        asin = p['asin']\n        if asin == 'nan' or len(asin) > 10:\n            continue\n\n        if asin in asins:\n            continue\n        else:\n            asins.add(asin)\n\n        products[i]['category'] = p['category']\n        products[i]['query'] = p['query']\n        products[i]['product_category'] = p['product_category']\n\n        products[i]['Title'] = p['name']\n        products[i]['Description'] = p['full_description']\n        products[i]['Reviews'] = all_reviews.get(asin, [])\n        products[i]['Rating'] = all_ratings.get(asin, 'N.A.')\n        for r in products[i]['Reviews']:\n            if 'score' not in r:\n                r['score'] = r.pop('stars')\n            if 'review' not in r:\n                r['body'] = ''\n            else:\n                r['body'] = r.pop('review')\n        products[i]['BulletPoints'] = p['small_description'] \\\n            if isinstance(p['small_description'], list) else [p['small_description']]\n\n        pricing = p.get('pricing')\n        if pricing is None or not pricing:\n            pricing = [100.0]\n            price_tag = '$100.0'\n        else:\n            pricing = [\n                float(Decimal(re.sub(r'[^\\d.]', '', price)))\n                for price in pricing.split('$')[1:]\n            ]\n            if len(pricing) == 1:\n                price_tag = f\"${pricing[0]}\"\n            else:\n                price_tag = f\"${pricing[0]} to ${pricing[1]}\"\n                pricing = pricing[:2]\n        products[i]['pricing'] = pricing\n        products[i]['Price'] = price_tag\n\n        options = dict()\n        customization_options = p['customization_options']\n        option_to_image = dict()\n        if customization_options:\n            for option_name, option_contents in customization_options.items():\n                if option_contents is None:\n                    continue\n                option_name = option_name.lower()\n\n                option_values = []\n                for option_content in option_contents:\n                    option_value = option_content['value'].strip().replace('/', ' | ').lower()\n                    option_image = option_content.get('image', None)\n\n                    option_values.append(option_value)\n                    option_to_image[option_value] = option_image\n                options[option_name] = option_values\n        products[i]['options'] = options\n        products[i]['option_to_image'] = option_to_image\n\n        # without color, size, price, availability\n        # if asin in attributes and 'attributes' in attributes[asin]:\n        #     products[i]['Attributes'] = attributes[asin]['attributes']\n        # else:\n        #     products[i]['Attributes'] = ['DUMMY_ATTR']\n        # products[i]['instruction_text'] = \\\n        #     attributes[asin].get('instruction', None)\n        # products[i]['instruction_attributes'] = \\\n        #     attributes[asin].get('instruction_attributes', None)\n\n        # without color, size, price, availability\n        if asin in attributes and 'attributes' in attributes[asin]:\n            products[i]['Attributes'] = attributes[asin]['attributes']\n        else:\n            products[i]['Attributes'] = ['DUMMY_ATTR']\n            \n        if human_goals:\n            if asin in human_attributes:\n                products[i]['instructions'] = human_attributes[asin]\n        else:\n            products[i]['instruction_text'] = \\\n                attributes[asin].get('instruction', None)\n\n            products[i]['instruction_attributes'] = \\\n                attributes[asin].get('instruction_attributes', None)\n\n        products[i]['MainImage'] = p['images'][0]\n        products[i]['query'] = p['query'].lower().strip()\n\n        all_products.append(products[i])\n\n    for p in all_products:\n        for a in p['Attributes']:\n            attribute_to_asins[a].add(p['asin'])\n\n    product_item_dict = {p['asin']: p for p in all_products}\n    product_prices = generate_product_prices(all_products)\n    return all_products, product_item_dict, product_prices, attribute_to_asins\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/engine/goal.py",
    "content": "\"\"\"\nFunctions for specifying goals and reward calculations.\n\"\"\"\nimport itertools\nimport random\nimport spacy\nfrom collections import defaultdict\nfrom rich import print\nfrom thefuzz import fuzz\nfrom .normalize import normalize_color\n\nnlp = spacy.load(\"en_core_web_lg\")\n\nPRICE_RANGE = [10.0 * i for i in range(1, 100)]\n\ndef get_goals(all_products, product_prices, human_goals=True, quiet: bool = False):\n    if human_goals:\n        return get_human_goals(all_products, product_prices, quiet=quiet)\n    else:\n        return get_synthetic_goals(all_products, product_prices, quiet=quiet)\n    \ndef get_human_goals(all_products, product_prices, quiet: bool = False):\n    goals = []\n    cnt_atts = defaultdict(int)\n    cnt = 0\n    for item in all_products:\n        asin = item['asin']\n        if 'instructions' not in item: continue\n        for product in item['instructions']:\n            attributes = product['instruction_attributes']\n            if len(attributes) == 0: \n                cnt += 1\n                continue\n\n            if product_prices is not None:\n                price = product_prices[asin]\n                price_range = [p for p in PRICE_RANGE if p > price][:4]\n                if len(price_range) >= 2:\n                    _, price_upper = sorted(random.sample(price_range, 2))\n                    price_text = \\\n                        f', and price lower than {price_upper:.2f} dollars'\n                else:\n                    price_upper = 1000000\n                    price_text = ''\n            else:\n                price_upper = 1000000\n\n            goals.append({\n                'asin': asin,\n                'category': item['category'],\n                'query': item['query'],\n                'name': item['name'],\n                'product_category': item['product_category'],\n                'instruction_text': product['instruction'].strip('.') + price_text,\n                'attributes': attributes,\n                'price_upper': price_upper,\n                'goal_options': product['instruction_options'],\n            })\n            for att in attributes:\n                cnt_atts[att] += 1\n            # goals += product_goals\n    for goal in goals:\n        goal['weight'] = 1\n    if not quiet:\n        print(len(all_products))\n        print(\"Number of Goals:\", len(goals))\n        print(cnt, 'skipped')\n    return goals\n\n\ndef get_synthetic_goals(all_products, product_prices, quiet: bool = False):\n    goals = []\n    cnt_atts = defaultdict(int)\n    for product in all_products:\n        if ('instruction_text' not in product or \n            product['instruction_text'] is None):\n            continue\n        product_goals = []        \n        asin = product['asin']\n        attributes = product['instruction_attributes']\n        assert len(attributes) > 0\n\n        if product_prices is not None:\n            price = product_prices[asin]\n            price_range = [p for p in PRICE_RANGE if p > price][:4]\n            if len(price_range) >= 2:\n                _, price_upper = sorted(random.sample(price_range, 2))\n                price_text = \\\n                    f', and price lower than {price_upper:.2f} dollars'\n            else:\n                price_upper = 1000000\n                price_text = ''\n        else:\n            price_upper = 1000000\n            price_text = ''\n\n        instruction_text = product['instruction_text']\n\n        options = product['options']\n        option_names = sorted(options)\n        combinations = list(itertools.product(\n            *(options[option_name] for option_name in option_names)\n        ))\n        for combination in combinations:\n            goal_options = dict()\n            for i, o in enumerate(combination):\n#                option_text.append(f'{option_names[i]}: {o}')\n                goal_options[option_names[i]] = o\n            option_text = ', and '.join([\n                f'{k}: {v}' for k, v in goal_options.items()\n            ])\n            option_text = ' with ' + option_text if option_text else ''\n            product_goals.append({\n                'asin': asin,\n                'category': product['category'],\n                'query': product['query'],\n                'name': product['name'],\n                'product_category': product['product_category'],\n                'instruction_text': f'{instruction_text}{option_text}{price_text}',\n                'attributes': attributes,\n                'price_upper': price_upper,\n                'goal_options': goal_options,\n                'name': product['Title'],\n            })\n            for att in attributes:\n                cnt_atts[att] += 1\n        goals += product_goals\n    for goal in goals:\n        goal['weight'] = sum(1. / cnt_atts[att] for att in goal['attributes']) / len(goal['attributes'])\n    return goals\n\n\ndef get_type_reward(purchased_product, goal):\n    \"\"\"Determines the type reward - captures whether chosen product is in the same category\"\"\"\n    query_match = purchased_product['query'] == goal['query']\n\n    # Check number of unique categories that match, ignoring order\n    purchased_product_category = [x.strip() for x in purchased_product['product_category'].split('›')]\n    goal_product_category = [x.strip() for x in goal['product_category'].split('›')]\n    category_match = len(set(purchased_product_category) & set(goal_product_category)) >= 2\n\n    # Determine whether types align based on product name similarity\n    purchased_type = purchased_product['name']\n    desired_type = goal['name']\n\n    purchased_type_parse = nlp(purchased_type)\n    desired_type_parse = nlp(desired_type)\n\n    purchased_type_parse = [t.text.lower() for t in purchased_type_parse if t.pos_ in ('PNOUN', 'NOUN', 'PROPN')]\n    desired_type_parse = [t.text.lower() for t in desired_type_parse if t.pos_ in ('PNOUN', 'NOUN', 'PROPN')]\n\n    n_intersect_type = len(\n        set(purchased_type_parse) & set(desired_type_parse)\n    )\n    if len(desired_type_parse) == 0:\n        title_score = 0.2\n    else:\n        title_score = n_intersect_type / len(desired_type_parse)\n\n    r_type = 1.0\n\n    # Adjust r_type score based on query, category title matching/scores\n    match = query_match or category_match or title_score > 0.2\n    if not match:\n        r_type = 0.5\n\n    if title_score < 0.1:\n        r_type = 0.1\n    \n    if title_score == 0.0:\n        r_type = 0.0\n\n    return dict(\n        r_type=r_type,\n        query_match=query_match,\n        category_match=category_match,\n        title_score=title_score,\n    )\n\n\ndef get_attribute_reward(purchased_product, goal):\n    \"\"\"Determines whether purchased products shares same attributes as goal\"\"\"\n    purchased_attrs = purchased_product['Attributes']\n    goal_attrs = goal['attributes']\n\n    num_attr_matches = 0\n    for g_attr in goal_attrs:\n        matched = False\n        # Check whether goal attribute found in purchased product attribute list\n        for p_attr in purchased_attrs:\n            score = fuzz.token_set_ratio(p_attr, g_attr)\n            if score > 85:\n                num_attr_matches += 1\n                matched = True\n                break\n        # If not in purchased attrs, check Title, Bullet Points (Features), Desc\n        if (\n            not matched and\n            (\n                g_attr in purchased_product['Title'].lower() or\n                g_attr in ' '.join(purchased_product['BulletPoints']).lower() or\n                g_attr in purchased_product['Description'].lower()\n            )\n        ):\n            num_attr_matches += 1\n            matched = True\n    \n    r_attr = num_attr_matches / len(goal_attrs)\n    return r_attr, num_attr_matches\n\n\ndef get_option_reward(purchased_options, goal_options):\n    \"\"\"Calculate reward for purchased product's options w.r.t. goal options\"\"\"\n    purchased_options = [normalize_color(o) for o in purchased_options]\n    goal_options = [normalize_color(o) for o in goal_options]\n\n    # Perform fuzzy matching of each purchased option against each goal option\n    num_option_matches = 0\n    for g_option in goal_options:\n        for p_option in purchased_options:\n            score = fuzz.token_set_ratio(p_option, g_option)\n            if score > 85:\n                num_option_matches += 1\n                break\n    \n    # Calculate option reward as fraction of goal options hit\n    r_option = num_option_matches / len(goal_options) if len(goal_options) > 0 else None\n    return r_option, num_option_matches\n\n\ndef get_reward(purchased_product, goal, price, options, **kwargs):\n    \"\"\"Get cumulative reward score for purchased product and goal\"\"\"\n    r_type_dict = get_type_reward(purchased_product, goal)\n\n    r_price = (\n        price <= goal['price_upper']\n    ) if goal['price_upper'] > 0 else None\n\n    r_att, num_attr_matches = get_attribute_reward(purchased_product, goal)\n\n    r_option, num_option_matches = get_option_reward(\n        list(options.values()),\n        goal['goal_options'].items()\n        if isinstance(goal['goal_options'], dict)\n        else goal['goal_options']\n    )\n\n    total_reward = (\n        (num_attr_matches + num_option_matches + r_price) \\\n            / (len(goal['attributes']) + len(goal['goal_options']) + 1)\n    )\n\n    total_reward *= r_type_dict['r_type']\n\n    # If verbose flag enabled, store score sub-components into dictionary\n    if kwargs.get('verbose', False):\n        info =  {\n            'r_type': r_type_dict['r_type'],\n            'r_att': r_att,\n            'w_att': len(goal['attributes']) / (len(goal['attributes']) + len(goal['goal_options']) + 1),\n            'query_match': r_type_dict['query_match'],\n            'category_match': r_type_dict['category_match'],\n            'title_score': r_type_dict['title_score'],\n        }\n        if r_option is not None:\n            info['r_option'] = r_option\n            info['w_option'] = len(goal['goal_options']) / (len(goal['attributes']) + len(goal['goal_options']) + 1)\n        if r_price is not None:\n            info['r_price'] = r_price\n            info['w_price'] = 1 / (len(goal['attributes']) + len(goal['goal_options']) + 1)\n        return total_reward, info\n    return total_reward\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/engine/normalize.py",
    "content": "import re\nfrom typing import Tuple\n\nCOLOR_SET = [\n    'alabaster', 'apricot', 'aqua', 'ash', 'asphalt', 'azure',\n    'banana', 'beige', 'black', 'blue', 'blush', 'bordeaux', 'bronze',\n    'brown', 'burgundy', 'camel', 'camo', 'caramel', 'champagne',\n    'charcoal', 'cheetah', 'chestnut', 'chocolate', 'christmas', 'coffee',\n    'cognac', 'copper', 'coral', 'cranberry', 'cream', 'crystal', 'dark',\n    'denim', 'eggplant', 'elephant', 'espresso', 'fuchsia', 'gold', 'granite',\n    'grape', 'graphite', 'grass', 'gray', 'green', 'grey', 'heather', 'indigo',\n    'ivory', 'ivy', 'khaki', 'lavender', 'lemon', 'leopard', 'light', 'lilac',\n    'lime', 'magenta', 'maroon', 'mauve', 'merlot', 'midnight', 'mint', 'mocha',\n    'multicolor', 'mushroom', 'mustard', 'natural', 'navy', 'nude', 'olive',\n    'orange', 'peach', 'pewter', 'pink',    'plum', 'purple', 'rainbow', 'red',\n    'rose', 'royal', 'rust', 'sand', 'sapphire', 'seashell', 'silver', 'skull',\n    'slate', 'steel', 'stone', 'stonewash', 'sunflower', 'tan', 'taupe', 'teal',\n    'tiger', 'turquoise', 'violet', 'walnut', 'wheat', 'white', 'wine', 'yellow',\n]\n\nSIZE_SET = [\n    'xx-large', '3x-large', '4x-large', '5x-large', 'x-large', 'x-small',\n    'medium', 'large', 'small',\n    'queen', 'twin', 'full', 'king', 'one size',\n    'pack',\n]\n\nSIZE_PATTERNS = [\n    re.compile(r'(.*)neck(.*)sleeve'),\n    re.compile(r'(.*) women \\| (.*) men'),\n    re.compile(r'(.*)w x(.*)l'),\n    re.compile(r'(.*)w by (.*)l'),\n    re.compile(r'(.*)w x(.*)h'),\n    re.compile(r'(.*)wide'),\n    re.compile(r'(.*)x-wide'),\n    re.compile(r'(.*)narrow'),\n    re.compile(r'(.*)petite'),\n    re.compile(r'(.*)inch'),\n    re.compile(r'(.*)plus'),\n    re.compile(r'(.*)mm'),\n    re.compile(r'women(.*)'),\n    re.compile(r'(.*)x(.*)'),\n    re.compile(r'(.*)ft'),\n    re.compile(r'(.*)feet'),\n    re.compile(r'(.*)meter'),\n    re.compile(r'(.*)yards'),\n    re.compile(r'(.*)\\*(.*)'),\n    re.compile(r'(.*)\\-(.*)'),\n    re.compile(r'(\\d+)\"$'),\n    re.compile(r'(\\d+)f$'),\n    re.compile(r'(\\d+)m$'),\n    re.compile(r'(\\d+)cm$'),\n    re.compile(r'(\\d+)g$'),\n]\nSIZE_PATTERNS = [re.compile(s) for s in SIZE_SET] + SIZE_PATTERNS\n\ndef normalize_color(color_string: str) -> str:\n    \"\"\"Extracts the first color found if exists\"\"\"\n    for norm_color in COLOR_SET:\n        if norm_color in color_string:\n            return norm_color\n    return color_string\n\ndef normalize_color_size(product_prices: dict) -> Tuple[dict, dict]:\n    \"\"\"Get mappings of all colors, sizes to corresponding values in COLOR_SET, SIZE_PATTERNS\"\"\"\n    \n    # Get all colors, sizes from list of all products\n    all_colors, all_sizes = set(), set()\n    for (_, color, size), _ in product_prices.items():\n        all_colors.add(color.lower())\n        all_sizes.add(size.lower())\n    \n    # Create mapping of each original color value to corresponding set value\n    color_mapping = {'N.A.': 'not_matched'} \n    for c in all_colors:\n        matched = False\n        for base in COLOR_SET:\n            if base in c:\n                color_mapping[c] = base\n                matched = True\n                break\n        if not matched:\n            color_mapping[c] = 'not_matched'\n\n    # Create mapping of each original size value to corresponding set value\n    size_mapping = {'N.A.': 'not_matched'}\n    for s in all_sizes:\n        matched = False\n        for pattern in SIZE_PATTERNS:\n            m = re.search(pattern, s)\n            if m is not None:\n                matched = True\n                size_mapping[s] = pattern.pattern\n                break\n        if not matched:\n            if s.replace('.', '', 1).isdigit():\n                size_mapping[s] = 'numeric_size'\n                matched= True\n        if not matched:\n            size_mapping[s] = 'not_matched'\n    \n    return color_mapping, size_mapping\n    \n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/envs/__init__.py",
    "content": "from gym.envs.registration import register\n\nfrom envs.webshop.src.webshop.web_agent_site.envs.web_agent_site_env import WebAgentSiteEnv\nfrom envs.webshop.src.webshop.web_agent_site.envs.web_agent_text_env import WebAgentTextEnv\n\nregister(\n  id='WebAgentSiteEnv-v0',\n  entry_point='envs.webshop.src.webshop.web_agent_site.envs:WebAgentSiteEnv',\n)\n\nregister(\n  id='WebAgentTextEnv-v0',\n  entry_point='envs.webshop.src.webshop.web_agent_site.envs:WebAgentTextEnv',\n)"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/envs/web_agent_site_env.py",
    "content": "import gym\nimport random\nimport requests\nimport string\nimport time\n\nfrom bs4 import BeautifulSoup\nfrom bs4.element import Comment\nfrom gym import spaces\nfrom os.path import join, dirname, abspath\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service\nfrom selenium.webdriver.chrome.options import Options\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.common.exceptions import ElementNotInteractableException\nfrom ..engine.engine import parse_action, END_BUTTON\n\nclass WebAgentSiteEnv(gym.Env):\n    \"\"\"Gym environment for HTML mode of WebShop environment\"\"\"\n\n    def __init__(self, observation_mode='html', **kwargs):\n        \"\"\"\n        Constructor for HTML environment\n\n        Arguments:\n        observation_mode (`str`) -- ['html' | 'text'] (default 'html')\n        pause (`float`) -- Pause (in seconds) after taking an action. \n            This is mainly for demo purposes.\n            Recommended value: 2.0s\n        render (`bool`) -- Show browser if set to `True`.\n        session ('str') -- Session ID to initialize environment with\n        \"\"\"\n        super(WebAgentSiteEnv, self).__init__()\n        self.observation_mode = observation_mode\n        self.kwargs = kwargs\n\n        # Create a browser driver to simulate the WebShop site\n        service = Service(join(dirname(abspath(__file__)), 'chromedriver'))\n        options = Options()\n        if 'render' not in kwargs or not kwargs['render']:\n            options.add_argument(\"--headless\")  # don't show browser\n        self.browser = webdriver.Chrome(service=service, options=options)\n\n        # Set flags and values for WebShop session\n        self.text_to_clickable = None\n        self.assigned_session = kwargs.get('session')\n        self.session = None\n        self.reset()\n\n    def step(self, action):\n        \"\"\"\n        Takes an action, updates WebShop environment, and returns (observation, reward, done, info)\n\n        Arguments:\n        action (`str`): An action should be of the following structure:\n          - search[keywords]\n          - click[value]\n        If action not valid, perform nothing.\n        \"\"\"\n        reward = 0.0\n        done = False\n        info = None\n\n        # Map action to executed command on the WebShop environment via the broswer driver\n        action_name, action_arg = parse_action(action)\n        if action_name == 'search':\n            try:\n                search_bar = self.browser.find_element_by_id('search_input')\n            except Exception:\n                pass\n            else:\n                search_bar.send_keys(action_arg)\n                search_bar.submit()\n        elif action_name == 'click':\n            try:\n                self.text_to_clickable[action_arg].click()\n            except ElementNotInteractableException:\n                # Perform force click with JavaScript\n                button = self.text_to_clickable[action_arg]\n                self.browser.execute_script(\"arguments[0].click();\", button)\n            reward = self.get_reward()\n            if action_arg == END_BUTTON:\n                done = True\n        elif action_name == 'end':\n            done = True\n        else:\n            print('Invalid action. No action performed.')\n\n        if 'pause' in self.kwargs:\n            time.sleep(self.kwargs['pause'])\n        return self.observation, reward, done, info\n    \n    def get_available_actions(self):\n        \"\"\"Returns list of available actions at the current step\"\"\"\n        # Determine if a search bar is available\n        try:\n            search_bar = self.browser.find_element_by_id('search_input')\n        except Exception:\n            has_search_bar = False\n        else:\n            has_search_bar = True\n\n        # Collect buttons, links, and options as clickables\n        buttons = self.browser.find_elements_by_class_name('btn')\n        product_links = self.browser.find_elements_by_class_name('product-link')\n        buying_options = self.browser.find_elements_by_css_selector(\"input[type='radio']\")\n\n        self.text_to_clickable = {\n            f'{b.text}': b\n            for b in buttons + product_links\n        }\n        for opt in buying_options:\n            opt_value = opt.get_attribute('value')\n            self.text_to_clickable[f'{opt_value}'] = opt\n        return dict(\n            has_search_bar=has_search_bar,\n            clickables=list(self.text_to_clickable.keys()),\n        )\n\n    def _parse_html(self, html=None, url=None):\n        \"\"\"\n        Returns web request result wrapped in BeautifulSoup object\n\n        Arguments:\n        url (`str`): If no url or html is provided, use the current\n            observation (HTML) for parsing.\n        \"\"\"\n        if html is None:\n            if url is not None:\n                html = requests.get(url)\n            else:\n                html = self.state['html']\n        html_obj = BeautifulSoup(html, 'html.parser')\n        return html_obj\n\n    def get_reward(self):\n        \"\"\"Get reward value at current step of the environment\"\"\"\n        html_obj = self._parse_html()\n        r = html_obj.find(id='reward')\n        r = float(r.findChildren(\"pre\")[0].string) if r is not None else 0.0\n        return r\n    \n    def get_instruction_text(self):\n        \"\"\"Get corresponding instruction text for environment current step\"\"\"\n        html_obj = self._parse_html(self.browser.page_source)\n        instruction_text = html_obj.find(id='instruction-text').h4.text\n        return instruction_text\n    \n    def convert_html_to_text(self, html):\n        \"\"\"Strip HTML of tags and add separators to convert observation into simple mode\"\"\"\n        texts = self._parse_html(html).find_all(string=True)\n        visible_texts = filter(tag_visible, texts)\n        observation = ' [SEP] '.join(t.strip() for t in visible_texts if t != '\\n')\n        return observation\n    \n    @property\n    def state(self):\n        \"\"\"\n        State that includes all information. The actual observation are\n        likely to be a subset or reduced form of the state.\n        \"\"\"\n        return dict(\n            url=self.browser.current_url,\n            html=self.browser.page_source,\n            instruction_text=self.instruction_text,\n        )\n    \n    @property\n    def observation(self):\n        \"\"\"Compiles state into either the `html` or `text` observation mode\"\"\"\n        html = self.state['html']\n        if self.observation_mode == 'html':\n            return html\n        elif self.observation_mode == 'text':\n            return self.convert_html_to_text(html)\n        else:\n            raise ValueError(\n                f'Observation mode {self.observation_mode} not supported.'\n            )\n\n    @property\n    def action_space(self):\n        # Recommended to use `get_available_actions` instead\n        return NotImplementedError\n\n    @property\n    def observation_space(self):\n        return NotImplementedError\n\n    def reset(self):\n        \"\"\"Create a new session and reset environment variables\"\"\"\n        if self.assigned_session is not None:\n            self.session = self.assigned_session\n        else:\n            self.session = ''.join(random.choices(string.ascii_lowercase, k=5))\n        init_url = f'http://127.0.0.1:3000/{self.session}'\n        self.browser.get(init_url)\n\n        self.instruction_text = self.get_instruction_text()\n\n        return self.observation, None\n\n    def render(self, mode='human'):\n        # TODO: Render observation in terminal or WebShop website\n        return NotImplementedError\n\n    def close(self):\n        # TODO: When DB used instead of JSONs, tear down DB here\n        self.browser.close()\n        print('Browser closed.')\n\ndef tag_visible(element):\n    \"\"\"Helper method to strip HTML block of extraneous tags\"\"\"\n    ignore = {'style', 'script', 'head', 'title', 'meta', '[document]'}\n    return (\n        element.parent.name not in ignore and not isinstance(element, Comment)\n    )\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/envs/web_agent_text_env.py",
    "content": "import os\nimport gym\nimport json\nimport random\nimport string\nimport time\nimport torch\nimport pickle\n\nfrom bs4 import BeautifulSoup\nfrom bs4.element import Comment\nfrom collections import defaultdict\nfrom flask import Flask\n\nfrom ..engine.engine import (\n    load_products,\n    init_search_engine,\n    get_top_n_product_from_keywords,\n    map_action_to_html,\n    parse_action,\n    get_product_per_page,\n    ACTION_TO_TEMPLATE,\n    END_BUTTON, NEXT_PAGE, PREV_PAGE, BACK_TO_SEARCH,\n)\nfrom ..engine.goal import get_reward, get_goals\nfrom ..utils import (\n    DEFAULT_FILE_PATH,\n    FEAT_CONV,\n    FEAT_IDS,\n    random_idx\n)\n\napp = Flask(__name__)\nclass WebAgentTextEnv(gym.Env):\n    \"\"\"Gym environment for Text mode of WebShop environment\"\"\"\n    def __init__(\n            self,\n            observation_mode='html',\n            file_path=DEFAULT_FILE_PATH,\n            server=None,\n            **kwargs\n        ):\n        \"\"\"\n        Constructor for text environment\n\n        Arguments:\n        observation_mode (`str`) -- ['html' | 'text'] (default 'html')\n        get_image\n        filter_goals\n        limit_goals\n        num_products\n        human_goals\n        session\n        session_prefix\n        show_attrs\n        \"\"\"\n        super(WebAgentTextEnv, self).__init__()\n        self.observation_mode = observation_mode\n        self.kwargs = kwargs\n\n        self.file_path = file_path\n\n        self.base_url = 'http://127.0.0.1:3000'\n        self.server = SimServer(\n            self.base_url,\n            self.file_path,\n            self.kwargs.get('filter_goals'),\n            self.kwargs.get('limit_goals', -1),\n            self.kwargs.get('num_products'),\n            self.kwargs.get('human_goals'),\n            self.kwargs.get('show_attrs', False),\n            self.kwargs.get('quiet', False),\n        ) if server is None else server\n        self.browser = SimBrowser(self.server)\n\n        self.session = self.kwargs.get('session')\n        self.session_prefix = self.kwargs.get('session_prefix')\n        if self.kwargs.get('get_image', 0):\n            self.feats = torch.load(FEAT_CONV)\n            self.ids = torch.load(FEAT_IDS)\n            self.ids = {url: idx for idx, url in enumerate(self.ids)}\n        self.prev_obs = []\n        self.prev_actions = []\n        self.num_prev_obs = self.kwargs.get('num_prev_obs', 0)\n        self.num_prev_actions = self.kwargs.get('num_prev_actions', 0)\n        self.reset()\n\n    def step(self, action):\n        \"\"\"\n        Takes an action, updates WebShop environment, and returns (observation, reward, done, info)\n\n        Arguments:\n        action (`str`): An action should be of the following structure:\n          - search[keywords]\n          - click[value]\n        If action not valid, perform nothing.\n        \"\"\"\n        info = None\n        self.get_available_actions()\n\n        # Determine action type (click, search) and argument\n        action_name, action_arg = parse_action(action)\n        action_name = action_name.lower()\n        if action_arg is not None:\n            action_arg = action_arg.lower()\n        if (action_name == 'search' and \n            action_arg is not None and \n            action_arg != ''):\n            status = self.browser.search(action_arg)\n        elif (action_name == 'click' and \n              action_arg in self.text_to_clickable.keys() and \n              action_arg != 'search'):\n            status = self.browser.click(action_arg, self.text_to_clickable)\n        else:\n            status = dict(reward=0, done=False)\n\n        # Update observation, state with the new action\n        ob = self.observation\n        text_list = [ob]\n        self.prev_actions.append(action)\n        for i in range(1, 1 + max(self.num_prev_obs, self.num_prev_actions)):\n            if len(self.prev_actions) >= i and self.num_prev_actions >= i:\n                text_list.append(self.prev_actions[-i])\n            if len(self.prev_obs) >= i and self.num_prev_obs >= i:\n                text_list.append(self.prev_obs[-i])\n        state = ' [SEP] '.join(text_list[::-1])\n        self.prev_obs.append(ob)\n        return state, status['reward'], status['done'], info\n\n    def get_available_actions(self):\n        \"\"\"Returns list of available actions at the current step\"\"\"\n        html_obj = self._parse_html()\n\n        # Collect search bar, buttons, links, and options as clickables\n        search_bar = html_obj.find(id='search_input')\n        has_search_bar = True if search_bar is not None else False\n        buttons = html_obj.find_all(class_='btn')\n        product_links  = html_obj.find_all(class_='product-link')\n        buying_options = html_obj.select('input[type=\"radio\"]')\n\n        self.text_to_clickable = {\n            f'{b.get_text()}'.lower(): b\n            for b in buttons + product_links\n        }\n        for opt in buying_options:\n            opt_value = opt.get('value')\n            self.text_to_clickable[f'{opt_value}'] = opt\n        return dict(\n            has_search_bar=has_search_bar,\n            clickables=list(self.text_to_clickable.keys()),\n        )\n    \n    def get_image(self):\n        \"\"\"Scrape image from page HTML and return as a list of pixel values\"\"\"\n        html_obj = self._parse_html(self.browser.page_source)\n        image_url = html_obj.find(id='product-image')\n        if image_url is not None:\n            image_url = image_url['src']\n            if image_url in self.ids:\n                image_idx = self.ids[image_url]\n                image = self.feats[image_idx]\n                return image\n        return torch.zeros(512)\n\n    def get_instruction_text(self):\n        \"\"\"Get corresponding instruction text for current environment session\"\"\"\n        html_obj = self._parse_html(self.browser.page_source)\n        instruction_text = html_obj.find(id='instruction-text').h4.text\n        return instruction_text\n\n    def _parse_html(self, html=None):\n        \"\"\"\n        Returns web request result wrapped in BeautifulSoup object\n\n        Arguments:\n        url (`str`): If no url or html is provided, use the current\n            observation (HTML) for parsing.\n        \"\"\"\n        if html is None:\n            html = self.state['html']\n        html_obj = BeautifulSoup(html, 'html.parser')\n        return html_obj\n    \n    @property\n    def observation(self):\n        \"\"\"Compiles state into either the `html` or `text` observation mode\"\"\"\n        html = self.state['html']\n        if self.observation_mode == 'html':\n            return html\n        elif self.observation_mode == 'text':\n            return self.convert_html_to_text(html, simple=True)\n        elif self.observation_mode == 'text_rich':\n            return self.convert_html_to_text(html, simple=False)\n        elif self.observation_mode == 'url':\n            return self.state['url']\n        else:\n            raise ValueError(\n                f'Observation mode {self.observation_mode} not supported.'\n            )\n    \n    @property\n    def state(self):\n        \"\"\"\n        State that includes all information. The actual observation are\n        likely to be a subset or reduced form of the state.\n        \"\"\"\n        return dict(\n            url=self.browser.current_url,\n            html=self.browser.page_source,\n            instruction_text=self.instruction_text,\n        )\n    \n    def convert_html_to_text(self, html, simple=False):\n        \"\"\"Strip HTML of tags and add separators to convert observation into simple mode\"\"\"\n        texts = self._parse_html(html).find_all(string=True)\n        visible_texts = filter(tag_visible, texts)\n        if simple:\n            # For `simple` mode, return just [SEP] separators\n            return ' [SEP] '.join(t.strip() for t in visible_texts if t != '\\n')\n        else:\n            # Otherwise, return an observation with tags mapped to specific, unique separators\n            observation = ''\n            for t in visible_texts:\n                if t == '\\n': continue\n                if t.parent.name == 'button':  # button\n                    processed_t = f'[button] {t} [button_]'\n                elif t.parent.name == 'label':  # options\n                    if f'\"{t}\"' in self.state['url']:\n                        processed_t = f'  [clicked button] {t} [clicked button_]'\n                        observation = f'You have clicked {t}.\\n' + observation\n                    else:\n                        processed_t = f'  [button] {t} [button_]'\n                elif t.parent.get('class') == [\"product-link\"]: # product asins\n                    if f'{t}' in self.server.user_sessions[self.session]['asins']:\n                        processed_t = f'\\n[clicked button] {t} [clicked button_]'\n                    else:\n                        processed_t = f'\\n[button] {t} [button_]'\n                else: # regular, unclickable text\n                    processed_t =  str(t)\n                observation += processed_t + '\\n'\n            return observation\n    \n    def reset(self, session=None, instruction_text=None):\n        \"\"\"Create a new session and reset environment variables\"\"\"\n        session_int = None\n        if session is not None:\n            self.session = str(session)\n            if isinstance(session, int):\n                session_int = session\n        else:\n            self.session = ''.join(random.choices(string.ascii_lowercase, k=10))\n        if self.session_prefix is not None:\n            self.session = self.session_prefix + self.session\n\n        init_url = f'{self.base_url}/{self.session}'\n        self.browser.get(init_url, session_id=self.session, session_int=session_int)\n\n        self.text_to_clickable = None\n        self.instruction_text = self.get_instruction_text() if instruction_text is None else instruction_text\n        obs = self.observation\n        self.prev_obs = [obs]\n        self.prev_actions = []\n        return obs, None\n\n    def render(self, mode='human'):\n        pass\n\n    def close(self):\n        pass\n    \n\ndef tag_visible(element):\n    ignore = {'style', 'script', 'head', 'title', 'meta', '[document]'}\n    return (\n        element.parent.name not in ignore and not isinstance(element, Comment)\n    )\n\n\nclass SimServer:\n    \"\"\"Lightweight simulator of WebShop Flask application for generating HTML observations\"\"\"\n    def __init__(\n        self,\n        base_url,\n        file_path,\n        filter_goals=None,\n        limit_goals=-1,\n        num_products=None,\n        human_goals=0,\n        show_attrs=False,\n        quiet=False,\n    ):\n        \"\"\"\n        Constructor for simulated server serving WebShop application\n        \n        Arguments:\n        filter_goals (`func`) -- Select specific goal(s) for consideration based on criteria of custom function\n        limit_goals (`int`) -- Limit to number of goals available\n        num_products (`int`) -- Number of products to search across\n        human_goals (`bool`) -- If true, load human goals; otherwise, load synthetic goals\n        \"\"\"\n        # Load all products, goals, and search engine\n        self.base_url = base_url\n        self.quiet = bool(quiet)\n        # cache_path = os.path.join(os.getcwd(), '.cache')\n        # if os.path.exists(cache_path):\n        #     self.all_products = pickle.load(open(os.path.join(cache_path, 'all_products.pkl'), 'rb'))\n        #     self.product_item_dict = pickle.load(open(os.path.join(cache_path, 'product_item_dict.pkl'), 'rb'))\n        #     self.product_prices = pickle.load(open(os.path.join(cache_path, 'product_prices.pkl'), 'rb'))\n        #     self.goals = pickle.load(open(os.path.join(cache_path, 'goals.pkl'), 'rb'))\n        # else:\n        #     self.all_products, self.product_item_dict, self.product_prices, _ = \\\n        #         load_products(filepath=file_path, num_products=num_products, human_goals=human_goals)\n        #     self.goals = get_goals(self.all_products, self.product_prices, human_goals)\n        #     os.mkdir(cache_path)\n        #     pickle.dump(self.all_products, open(os.path.join(cache_path, 'all_products.pkl'), 'wb'))\n        #     pickle.dump(self.product_item_dict, open(os.path.join(cache_path, 'product_item_dict.pkl'), 'wb'))\n        #     pickle.dump(self.product_prices, open(os.path.join(cache_path, 'product_prices.pkl'), 'wb'))\n        #     pickle.dump(self.goals, open(os.path.join(cache_path, 'goals.pkl'), 'wb'))\n        \n        # self.search_engine = init_search_engine(num_products=num_products)\n        \n        self.all_products, self.product_item_dict, self.product_prices, _ = \\\n            load_products(filepath=file_path, num_products=num_products, human_goals=human_goals, quiet=self.quiet)\n        self.search_engine = init_search_engine(num_products=num_products)\n        self.goals = get_goals(self.all_products, self.product_prices, human_goals, quiet=self.quiet)\n\n        self.show_attrs = show_attrs\n\n        # Fix outcome for random shuffling of goals\n        random.seed(233)\n        random.shuffle(self.goals)\n\n        # Apply `filter_goals` parameter if exists to select speific goal(s)\n        if filter_goals is not None:\n            self.goals = [\n                goal for (i, goal) in enumerate(self.goals)\n                if filter_goals(i, goal)\n            ]\n        \n        # Imposes `limit` on goals via random selection\n        if limit_goals != -1 and limit_goals < len(self.goals):\n            self.weights = [goal['weight'] for goal in self.goals]\n            self.cum_weights = [0]\n            for w in self.weights:\n                self.cum_weights.append(self.cum_weights[-1] + w)\n            idxs = []\n            while len(idxs) < limit_goals:\n                idx = random_idx(self.cum_weights)\n                if idx not in idxs:\n                    idxs.append(idx)\n            self.goals = [self.goals[i] for i in idxs]\n        if not self.quiet:\n            print(f'Loaded {len(self.goals)} goals.')\n        # pickle.dump(self.goals, open(os.path.join(cache_path, 'goals_final.pkl'), 'wb'))\n\n        # Set extraneous housekeeping variables\n        self.weights = [goal['weight'] for goal in self.goals]\n        self.cum_weights = [0]\n        for w in self.weights:\n            self.cum_weights.append(self.cum_weights[-1] + w)\n        self.user_sessions = dict()\n        self.search_time = 0\n        self.render_time = 0\n        self.sample_time = 0\n        self.assigned_instruction_text = None  # TODO: very hacky, should remove\n        \n    @app.route('/', methods=['GET', 'POST'])\n    def index(self, session_id, **kwargs):\n        \"\"\"Redirect to the search page with the given session ID\"\"\"\n        html = map_action_to_html(\n            'start',\n            session_id=session_id,\n            instruction_text=kwargs['instruction_text'],\n        )\n        url = f'{self.base_url}/{session_id}'\n        return html, url\n    \n    @app.route('/', methods=['GET', 'POST'])\n    def search_results(self, session_id, **kwargs):\n        \"\"\"Initialize session and return the search results page\"\"\"\n        session = self.user_sessions[session_id]\n        keywords = kwargs['keywords']  # TODO: why is this using kwargs? why not session?\n        assert isinstance(keywords, list)\n        page = 1 if 'page' not in kwargs else kwargs['page']\n        session[\"page\"] = page\n        session[\"keywords\"] = keywords\n        session[\"actions\"][\"search\"] += 1\n        session[\"asin\"] = None\n        session[\"options\"] = {}\n\n        # Perform search on keywords from items and record amount of time it takes\n        old_time = time.time()\n        top_n_products = get_top_n_product_from_keywords(\n            keywords,\n            self.search_engine,\n            self.all_products,\n            self.product_item_dict,\n        )\n        self.search_time += time.time() - old_time\n        \n        # Get product list from search result asins and get list of corresponding URLs\n        products = get_product_per_page(top_n_products, page)\n\n        keywords_url_string = '+'.join(keywords)\n        url = (\n            f'{self.base_url}/search_results/{session_id}/'\n            f'{keywords_url_string}/{page}'\n        )\n\n        # Render HTML search page and record amount of time taken\n        old_time = time.time()\n        html = map_action_to_html(\n            'search',\n            session_id=session_id,\n            products=products,\n            keywords=session[\"keywords\"],\n            page=page,\n            total=len(top_n_products),\n            instruction_text=session[\"goal\"][\"instruction_text\"],\n        )\n        self.render_time += time.time() - old_time\n        return html, url\n    \n    @app.route('/', methods=['GET', 'POST'])\n    def item_page(self, session_id, **kwargs):\n        \"\"\"Render and return the HTML for a product item page\"\"\"\n        session = self.user_sessions[session_id]\n        clickable_name = kwargs['clickable_name']\n        text_to_clickable = kwargs['text_to_clickable']\n        clickable = text_to_clickable[clickable_name]\n\n        # Update session logs with information of last product asin selected\n        if (clickable.get('class') is not None and\n            clickable.get('class')[0] == 'product-link'):\n            session[\"asin\"] = clickable_name.upper()\n            session[\"actions\"][\"asin\"] += 1\n            session[\"asins\"].add(session[\"asin\"])\n        elif clickable.get('name') is not None:\n            clickable_key = clickable['name'].lower()\n            session[\"options\"][clickable_key] = clickable_name\n            session[\"actions\"][\"options\"] += 1\n\n        # Set fields + url of page, then render page's HTML\n        product_info = self.product_item_dict[session[\"asin\"]]\n        keywords_url_string = '+'.join(session[\"keywords\"])\n        option_string = json.dumps(session['options'])\n\n        url = (\n            f'{self.base_url}/item_page/{session_id}/'\n            f'{session[\"asin\"]}/{keywords_url_string}/'\n            f'{session[\"page\"]}/{option_string}'\n        )\n\n        html = map_action_to_html(\n            'click',\n            session_id=session_id,\n            product_info=product_info,\n            keywords=session[\"keywords\"],\n            page=session[\"page\"],\n            asin=session[\"asin\"],\n            options=session[\"options\"],\n            instruction_text=session[\"goal\"][\"instruction_text\"],\n            show_attrs=self.show_attrs,\n        )\n        return html, url\n\n    @app.route('/', methods=['GET', 'POST'])\n    def item_sub_page(self, session_id, **kwargs):\n        \"\"\"Render and return the HTML for a product's sub page (i.e. description, features)\"\"\"\n        session = self.user_sessions[session_id]\n        clickable_name = kwargs['clickable_name']\n        for k in ACTION_TO_TEMPLATE:\n            if clickable_name.lower() == k.lower():\n                clickable_name = k\n                break\n        \n        # Set fields + url of page, then render page's HTML\n        product_info = self.product_item_dict[session[\"asin\"]]\n        session[\"actions\"][clickable_name] += 1\n        keywords_url_string = '+'.join(session[\"keywords\"])\n        url = (\n            f'{self.base_url}/item_sub_page/{session_id}/'\n            f'{session[\"asin\"]}/{keywords_url_string}/{session[\"page\"]}/'\n            f'{clickable_name}/{session[\"options\"]}'\n        )\n        html = map_action_to_html(\n            f'click[{clickable_name}]',\n            session_id=session_id,\n            product_info=product_info,\n            keywords=session[\"keywords\"],\n            page=session[\"page\"],\n            asin=session[\"asin\"],\n            options=session[\"options\"],\n            instruction_text=session[\"goal\"][\"instruction_text\"],\n        )\n        return html, url\n\n    @app.route('/', methods=['GET', 'POST'])\n    def done(self, session_id, **kwargs):\n        \"\"\"Render and return HTML for done page\"\"\"\n        session = self.user_sessions[session_id]\n        goal = self.user_sessions[session_id]['goal']\n        purchased_product = self.product_item_dict[session[\"asin\"]]\n        session[\"actions\"][\"purchase\"] += 1\n        price = self.product_prices.get(session[\"asin\"])\n\n        # Calculate reward for selected product and set variables for page details\n        reward, info = get_reward(\n            purchased_product,\n            goal,\n            price=price,\n            options=session[\"options\"],\n            verbose=True\n        )\n\n        self.user_sessions[session_id]['verbose_info'] = info\n        self.user_sessions[session_id]['done'] = True\n        self.user_sessions[session_id]['reward'] = reward\n\n        url = (\n            f'{self.base_url}/done/{session_id}/'\n            f'{session[\"asin\"]}/{session[\"options\"]}'\n        )\n        html = map_action_to_html(\n            f'click[{END_BUTTON}]',\n            session_id=session_id,\n            reward=reward,\n            asin=session[\"asin\"],\n            options=session[\"options\"],\n            instruction_text=session[\"goal\"][\"instruction_text\"],\n        )\n        return html, url, reward\n    \n    def receive(self, session_id, current_url, session_int=None, **kwargs):\n        \"\"\"Map action to the corresponding page\"\"\"\n        status = dict(reward=0.0, done=False)\n\n        with app.app_context(), app.test_request_context():\n            # Create/determine goal, instruction_text from current session\n            if session_id not in self.user_sessions:\n                idx = session_int if (session_int is not None and isinstance(session_int, int)) else random_idx(self.cum_weights) \n                goal = self.goals[idx]\n                instruction_text = goal['instruction_text']\n                self.user_sessions[session_id] = {'goal': goal, 'done': False}\n            else:\n                instruction_text = \\\n                    self.user_sessions[session_id]['goal']['instruction_text']\n            if self.assigned_instruction_text is not None:\n                instruction_text = self.assigned_instruction_text  # TODO: very hacky, should remove\n                self.user_sessions[session_id]['goal']['instruction_text'] = instruction_text\n            session = self.user_sessions[session_id]\n\n            if not kwargs:\n                # If no action, reset the session variables\n                kwargs['instruction_text'] = instruction_text\n                html, url = self.index(session_id, **kwargs)\n                self.user_sessions[session_id].update(\n                    {\n                        'keywords': None,\n                        'page': None,\n                        'asin': None,\n                        'asins': set(),\n                        'options': dict(),\n                        'actions': defaultdict(int)\n                    }\n                )\n            elif 'keywords' in kwargs:\n                # If search keywords are available, run a search\n                html, url = self.search_results(session_id, **kwargs)\n            elif 'clickable_name' in kwargs:\n                clickable_name = kwargs['clickable_name'].lower()\n                if clickable_name == END_BUTTON.lower():\n                    # If \"buy now\" clicked, calculate reward and flag session as terminated\n                    html, url, reward = self.done(session_id, **kwargs)\n                    status['reward'] = reward\n                    status['done'] = True\n                elif clickable_name == BACK_TO_SEARCH.lower():\n                    # If \"back to search\" clicked, recursively reset the session back to search page\n                    html, url, status = self.receive(session_id, current_url)\n                elif (clickable_name == NEXT_PAGE.lower() and \n                      self.get_page_name(current_url) == 'search_results'):\n                    # If \"next page\" clicked from search results, re-render with `page` enumerated\n                    html, url, status = self.receive(\n                        session_id,\n                        current_url,\n                        keywords=session[\"keywords\"],\n                        page=session[\"page\"] + 1,\n                    )\n                elif (clickable_name == PREV_PAGE.lower() and \n                      self.get_page_name(current_url) == 'search_results'):\n                    # If \"prev page\" clicked from search results, re-render with `page` denumerated\n                    html, url, status = self.receive(\n                        session_id,\n                        current_url,\n                        keywords=session[\"keywords\"],\n                        page=session[\"page\"] - 1,\n                    )\n                elif (clickable_name == PREV_PAGE.lower() and \n                      self.get_page_name(current_url) == 'item_sub_page'):\n                    # If \"prev page\" clicked from sub page, return to corresponding item page\n                    html, url = self.item_page(session_id, **kwargs)\n                elif (clickable_name == PREV_PAGE.lower() and \n                      self.get_page_name(current_url) == 'item_page'):\n                    # If \"prev page\" clicked from item page, return to search results page\n                    html, url = self.search_results(\n                        session_id,\n                        keywords=session[\"keywords\"],\n                        page=session[\"page\"],\n                        **kwargs\n                    )\n                elif clickable_name in [k.lower() for k in ACTION_TO_TEMPLATE]:\n                    # Render item_sub_page if clickable is description, features, or reviews\n                    html, url = self.item_sub_page(session_id, **kwargs)\n                else:\n                    # Otherwise, render current item page\n                    html, url = self.item_page(session_id, **kwargs)\n            return html, url, status\n    \n    def get_page_name(self, url):\n        \"\"\"Determine which page (i.e. item_page, search_results) the given URL is pointing at\"\"\"\n        if url is None:\n            return None\n        page_names = [\n            'search_results',\n            'item_page',\n            'item_sub_page',\n            'done'\n        ]\n        for page_name in page_names:\n            if page_name in url:\n                return page_name\n        return ''  # index page\n\n\nclass SimBrowser:\n    \"\"\"Simulated browser for rendering the HTML source of WebShop environment pages\"\"\"\n    def __init__(self, server):\n        self.server = server\n        self.current_url = None\n        self.page_source = None\n        self.session_id = None\n\n    def get(self, url, session_id=None, session_int=None):\n        \"\"\"Set browser variables to corresponding link, page HTML for URL\"\"\"\n        self.session_id = url.split('/')[-1] if session_id is None else session_id\n        self.page_source, _, _ = \\\n            self.server.receive(self.session_id, self.current_url, session_int=session_int)\n        self.current_url = url\n    \n    def click(self, clickable_name, text_to_clickable):\n        \"\"\"Wrapper for `receive` handler for performing click action on current page\"\"\"\n        self.page_source, self.current_url, status = \\\n            self.server.receive(\n                self.session_id,\n                current_url=self.current_url,\n                clickable_name=clickable_name,\n                text_to_clickable=text_to_clickable,\n            )\n        return status\n    \n    def search(self, keywords):\n        \"\"\"Wrapper for `receive` handler for performing search action on current page\"\"\"\n        if isinstance(keywords, str):\n            keywords = keywords.split(' ')\n        self.page_source, self.current_url, status = \\\n            self.server.receive(\n                self.session_id,\n                current_url=self.current_url,\n                keywords=keywords,\n        )\n        return status\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/models/__init__.py",
    "content": "from .models import (\n    HumanPolicy,\n    RandomPolicy,\n)\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/models/models.py",
    "content": "\"\"\"\nModel implementations. The model interface should be suitable for both\nthe ``site env'' and the ``text env''.\n\"\"\"\nimport random\n\nrandom.seed(4)\n\n\nclass BasePolicy:\n    def __init__(self):\n        pass\n    \n    def forward(observation, available_actions):\n        \"\"\"\n        Args:\n            observation (`str`):\n                HTML string\n\n            available_actions ():\n                ...\n        Returns:\n            action (`str`): \n                Return string of the format ``action_name[action_arg]''.\n                Examples:\n                    - search[white shoes]\n                    - click[button=Reviews]\n                    - click[button=Buy Now]\n        \"\"\"\n        raise NotImplementedError\n\n\nclass HumanPolicy(BasePolicy):\n    def __init__(self):\n        super().__init__()\n\n    def forward(self, observation, available_actions):\n        action = input('> ')\n        return action\n\n\nclass RandomPolicy(BasePolicy):\n    def __init__(self):\n        super().__init__()\n    \n    def forward(self, observation, available_actions):\n        if available_actions['has_search_bar']:\n            action = 'search[shoes]'\n        else:\n            action_arg = random.choice(available_actions['clickables'])\n            action = f'click[{action_arg}]'\n        return action\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/static/style.css",
    "content": ".text {\n    font-family: Arial, Helvetica;\n}\n\n* {\n    -webkit-border-radius: 1px !important;\n       -moz-border-radius: 1px !important;\n            border-radius: 1px !important;\n  }\n\n#logo {\n    color: #666;\n    width:100%;   \n}\n\n#logo h1 {\n    font-size: 60px;\n    text-shadow: 1px 2px 3px #999;\n    font-family: Roboto, sans-serif;\n    font-weight: 700;\n    letter-spacing: -1px;\n}\n\n#logo p{\n    padding-bottom: 20px;\n}\n  \n#thankyou {\n    color: #666;\n    width:100%;\n    font-size: 60px;\n    font-family: Roboto, sans-serif;\n    font-weight: 700;\n    padding-bottom: 20px;\n    letter-spacing: -1px;\n}\n\n#form-buscar >.form-group >.input-group > .form-control {\n    height: 40px;\n}\n\n#form-buscar >.form-group >.input-group > .input-group-btn > .btn{\n    height: 40px;\n    font-size: 16px;\n    font-weight: 300;      \n}\n\n#form-buscar >.form-group >.input-group > .input-group-btn > .btn .glyphicon{\n    margin-right:12px;   \n}    \n    \n#form-buscar >.form-group >.input-group > .form-control {\n    font-size: 16px;\n    font-weight: 300;\n}\n  \n#form-buscar >.form-group >.input-group > .form-control:focus {\n    border-color: #33A444;\n    outline: 0;\n    -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 1px rgba(0, 109, 0, 0.8);\n    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 1px rgba(0, 109, 0, 0.8);\n}\n\nbody {\n    background: white;\n    min-height: 100vh\n}\n\n.text-gray {\n    color: #aaa\n}\n\n.result-img {\n    max-height: 300px;\n    max-width: 300px;\n    overflow: hidden;\n}\n\n.item-page-img {\n    max-height: 600px;\n    max-width: 370px;\n    overflow: hidden;\n}\n\n.top-buffer { margin-top:10px; }\n\n.product-info {\n    font-size: 18px;\n}\n\n.star-active {\n    color: #FBC02D;\n    margin-top: 10px;\n    margin-bottom: 10px\n}\n\n.star-active:hover {\n    color: #F9A825;\n    cursor: pointer\n}\n\n.star-inactive {\n    color: #CFD8DC;\n    margin-top: 10px;\n    margin-bottom: 10px\n}\n\n.blue-text {\n    color: #116396\n}\n\n.btn {\n    margin-left: 0px;\n    margin-right: 0px;\n}\n/* Boostrap Buttons Styling */\n  \n.btn-primary {\n  font-size: 13px;\n  color: rgba(58, 133, 191, 0.75);\n  letter-spacing: 1px;\n  line-height: 15px;\n  border: 2px solid rgba(58, 133, 191, 0.75);\n  border-radius: 40px;\n  background: transparent;\n}\n\n.btn-primary:hover {\n  color: #FFF;\n  background: rgba(58, 133, 191, 0.75);\n}\n\n.btn-success {\n  font-size: 13px;\n  color: rgba(103, 192, 103, 0.75);\n  letter-spacing: 1px;\n  line-height: 15px;\n  border: 2px solid rgba(103, 192, 103, 0.75);\n  border-radius: 40px;\n  background: transparent;\n}\n\n.btn-success:hover {\n  color: #FFF;\n  background: rgb(103, 192, 103, 0.75);\n}\n\n.btn.purchase {\n  color: rgb(0, 0, 0);\n  background: rgb(250, 167, 13);\n}\n\n.btn.purchase:hover {\n  color: rgb(0, 0, 0);\n  background: rgb(253, 199, 98);\n}\n\n.radio-toolbar {\n  margin: 5px;\n}\n\n.radio-toolbar input[type=\"radio\"] {\n  opacity: 0;\n  position: fixed;\n  width: 0;\n}\n\n.radio-toolbar label {\n    display: inline-block;\n    background-color: rgb(245, 241, 241);\n    padding: 10px 10px;\n    font-size: 14px;\n    border: 1px solid #444;\n    border-radius: 4px;\n}\n\n.radio-toolbar label:hover {\n  background-color: rgb(255, 247, 217);\n}\n\n.radio-toolbar input[type=\"radio\"]:focus + label {\n    border: 1px solid #444;\n}\n\n.radio-toolbar input[type=\"radio\"]:checked + label {\n    background-color: rgb(255, 234, 163);\n    border: 1px solid #444;\n}\n\n#instruction-text {\n    margin-top:10px;\n    margin-bottom:10px;\n    border: #797474 solid;\n    border-radius: 20px;\n    padding: 5px;\n}\n\npre {\n    white-space: pre-line;\n}"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/attributes_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}\">\n          <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-md-12\">\n          <div class=\"row top-buffer\">\n            <div class=\"col-sm-6\" name=\"description\">\n              <div class=\"card card-body\">\n                <ul>\n                  {% for attribute in product_info.Attributes %}\n                  <li><p class=\"attribute\"> {{attribute}}</p></li>\n                  {% endfor %}\n                </ul>\n              </div>\n            </div>\n            <div class=\"col-sm-6\" name=\"description\">\n              <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                <h5 class=\"font-weight-bold my-2 product-category\">{{product_info.category}}</h5>\n              </div>\n              <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                <h5 class=\"font-weight-bold my-2 product-query\">{{product_info.query}}</h5>\n              </div>\n              <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                <h5 class=\"font-weight-bold my-2 product-product_category\">{{product_info.product_category}}</h5>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/description_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}\">\n          <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-md-12\">\n          <div class=\"row top-buffer\">\n            <div class=\"col-sm-6\" name=\"description\">\n              <div class=\"card card-body\">\n                <p class=\"product-info\">{{product_info.Description}}</p>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/done_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <!-- Code reference: https://bootsnipp.com/snippets/m13mN -->\n    <div class=\"container\" style=\"margin-top: 8%;\">\n      <div class=\"col-md-6 col-md-offset-3\">     \n        <div class=\"row\">\n          <div id=\"thankyou\" class=\"text-center\">\n            <h1>Thank you for shopping with us!</h1>\n          </div>\n          <div id=\"stats\" class=\"text-center\">\n            <h3 align=\"mturk_code\">Your code: </h3>\n            <p><pre>{{ mturk_code }}</pre> (Paste it in your MTurk interface.)</p>\n            <div style=\"display:none\">\n              <h2 align=\"left\">Purchased</h2>\n              <hr class=\"solid\">\n              <h4 id=\"asin\">asin<pre>{{ asin }}</pre></p>\n              <h4 id=\"options\">options<pre>{{ options | tojson }}</pre></h4>\n              <h4 id=\"purchased_attrs\">attrs<pre>{{ purchased_attrs }}</pre></h4>\n              <h4 id=\"purchased-category\">category<pre>{{ category }}</pre></h4>\n              <h4 id=\"purchased-query\">query<pre>{{ query }}</pre></h4>\n              <h4 id=\"purchased-pc\">product category<pre>{{ product_category }}</pre></h4>\n              <h2 align=\"left\">Target</h2>\n              <hr class=\"solid\">\n              <h4 id=\"goal-asin\">asin<pre>{{ goal.asin }}</pre></p>\n              <h4 id=\"goal-options\">options<pre>{{ goal.goal_options }}</pre></h4>\n              <h4 id=\"goal-attrs\">attrs<pre>{{ goal.attributes }}</pre></h4>\n              <h4 id=\"goal-price\">price upper<pre>{{ goal.price_upper }}</pre></h4>\n              <h4 id=\"goal-instruction-text\">instuction text<pre>{{ goal.instruction_text }}</pre></h4>\n              <h4 id=\"goal-category\">category<pre>{{ goal.category }}</pre></h4>\n              <h4 id=\"goal-pc\">product category<pre>{{ goal.product_category }}</pre></h4>\n              <h4 id=\"goal-query\">query<pre>{{ goal.query }}</pre></h4>\n              <h4>Goal <div id=\"goal\"><pre>{{ goal | pprint }}</pre></div></h4>\n              <h2 align=\"left\">Reward</h2>\n            </div>\n            <hr class=\"solid\">\n      \t\t\t<h3   id=\"reward\">Your score (min 0.0, max 1.0)<pre>{{ reward }}</pre></h3>\n            <h4 hidden>Reward Details <div id=\"reward_info\"><pre>{{ reward_info | pprint }}</pre></div></h4>\n          </div>\n        </div>            \n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/features_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}\">\n          <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-md-12\">\n          <div class=\"row top-buffer\">\n            <div class=\"col-sm-6\" name=\"bulletpoints\">\n              <div class=\"card card-body\">\n                <ul>\n                  {% for bulletpoint in product_info.BulletPoints %}\n                  <li><p class=\"product-info\"> {{bulletpoint}}</p></li>\n                  {% endfor %}\n                </ul>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/item_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('search_results', session_id=session_id, keywords=keywords, page=page)}}\">\n          <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-md-4 mb-4 mb-md-0\">\n          <div class=\"row top-buffer\">\n            <img id=\"product-image\" src=\"{{product_info.MainImage}}\" class=\"item-page-img\">\n          </div>\n          {% for option_name, option_contents in product_info.options.items() %}\n            <div class=\"row top-buffer\">\n              <h4>{{ option_name }}</h4>\n              <div class=\"radio-toolbar\">\n                {% for option_content in option_contents %}\n                  {% set current_options = options.copy() %}\n                  {% set _ = current_options.update({option_name: option_content}) %}\n                  {% set url = url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=current_options) %}\n                  <input type=\"radio\" id=\"radio_{{ option_name }}{{ loop.index0 }}\" name=\"{{ option_name }}\" value=\"{{ option_content }}\" data-url=\"{{ url }}\">\n                  <label for=\"radio_{{ option_name }}{{ loop.index0 }}\">{{ option_content }}</label>\n                {% endfor %}\n              </div>\n            </div>\n          {% endfor %}\n        </div>\n        <div class=\"col-md-6\">\n          <h2>{{product_info.Title}}</h2>\n          <h4>Price: {{product_info.Price}}</h4>\n          <h4>Rating: {{product_info.Rating}}</h4>\n          <div class=\"row top-buffer\">\n            <div class=\"col-sm-3\" name=\"description\">\n              <form method=\"post\" action=\"{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Description', options=options) }}\">\n                <button class=\"btn btn-primary\" type=\"submit\">Description</button>\n              </form>\n            </div>\n            <div class=\"col-sm-3\" name=\"bulletpoints\">\n              <form method=\"post\" action=\"{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Features', options=options) }}\">\n                <button class=\"btn btn-primary\" type=\"submit\">Features</button>\n              </form>\n            </div>\n            <div class=\"col-sm-3\" name=\"reviews\">\n              <form method=\"post\" action=\"{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Reviews', options=options) }}\">\n                <button class=\"btn btn-primary\" type=\"submit\">Reviews</button>\n              </form>\n            </div>\n            {% if show_attrs %}\n            <div class=\"col-sm-3\" name=\"attributes\">\n              <form method=\"post\" action=\"{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Attributes', options=options) }}\">\n                <button class=\"btn btn-primary\" type=\"submit\">Attributes</button>\n              </form>\n            </div>\n            {% endif %}\n          </div>\n        </div>\n        <div class=\"col-sm-2\">\n          <div class=\"row top-buffer\">\n            <form method=\"post\" action=\"{{url_for('done', session_id=session_id, asin=asin, options=options )}}\">\n              <button type=\"submit\" class=\"btn btn-lg purchase\">Buy Now</button>\n            </form>\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n  <script>\n    $(document).ready(function() {\n      $('input:radio').each(function() {\n        //console.log($(this).val());\n        let options = JSON.parse(`{{ options | tojson }}`);\n        let optionValues = $.map(options, function(value, key) { return value });\n        //console.log(optionValues);\n        if (optionValues.includes($(this).val())) {\n          $(this).prop('checked', true);\n\n          let option_to_image = JSON.parse(`{{ product_info.option_to_image | tojson }}`);\n//          console.log($(this).val());\n//          console.log(options);\n//          console.log(option_to_image);\n          let image_url = option_to_image[$(this).val()];\n\n          //console.log(image_url);\n          if (image_url) {\n            $(\"#product-image\").attr(\"src\", image_url);\n          }\n        }\n        \n        // reload with updated options\n        this.addEventListener(\"click\", function() {\n          window.location.href = this.dataset.url;\n        });\n\n      });\n    });\n  </script>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/results_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{ url_for('index', session_id=session_id) }}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-8\">\n          <div class=\"display-4\">\n            <h3>Page {{page}} (Total results: {{total}})</h3>\n          </div>\n          {% if page > 1 %}\n            <div class=\"col-sm-2\">\n              <form method=\"post\" action=\"{{url_for('search_results', session_id=session_id, keywords=keywords, page=page - 1)}}\">\n                <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n              </form>\n            </div>\n            <div class=\"col-sm-2\">\n              <form method=\"post\" action=\"{{url_for('search_results', session_id=session_id, keywords=keywords, page=page + 1)}}\">\n                <button type=\"submit\" class=\"btn btn-primary\">Next &gt;</button>\n              </form>\n            </div>\n          {% else %}\n            <div class=\"col-sm-2\">\n            </div>\n            <div class=\"col-sm-2\">\n              <form method=\"post\" action=\"{{url_for('search_results', session_id=session_id, keywords=keywords, page=page + 1)}}\">\n                <button type=\"submit\" class=\"btn btn-primary\">Next &gt;</button>\n              </form>\n            </div>\n          {% endif %}\n        </div>\n      </div>\n\n      <div class=\"row top-buffer\">\n        {% for item in products %}\n        <div class=\"col-lg-12 mx-auto list-group-item\">\n          <div class=\"col-lg-4\">\n            <img src=\"{{item.MainImage}}\" class=\"result-img\">\n          </div>\n          <div class=\"col-lg-8\">\n            <ul class=\"list-group shadow\">\n                <div class=\"media align-items-lg-center flex-column flex-lg-row p-3\">\n                  <div class=\"media-body order-2 order-lg-1 searched-product\">\n                    {% set item_page_url = url_for('item_page', session_id=session_id, asin=item.asin, keywords=keywords, page=page, options=dict() ) %}\n                    <h4 class=\"mt-0 font-weight-bold mb-2 product-asin\"><a class=\"product-link\" href=\"{{ item_page_url }}\">{{item.asin}}</a></h5>\n                    <h4 class=\"mt-0 font-weight-bold mb-2 product-title\">{{item.Title}}</h5>\n                    <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                      <h5 class=\"font-weight-bold my-2 product-price\">{{item.Price}}</h6>\n                    </div>\n<!--\n                    <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                      <h5 class=\"font-weight-bold my-2 product-category\">{{item.category}}</h5>\n                    </div>\n                    <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                      <h5 class=\"font-weight-bold my-2 product-query\">{{item.query}}</h5>\n                    </div>\n                    <div class=\"d-flex align-items-center justify-content-between mt-1\">\n                      <h5 class=\"font-weight-bold my-2 product-product_category\">{{item.product_category}}</h5>\n                    </div>\n-->\n                  </div>\n                </div>\n            </ul>\n          </div>\n        </div>\n        {% endfor %}\n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/review_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css\t\">\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js\"></script>\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <div class=\"container py-5\">\n      <div class=\"row top-buffer\">\n        <div class=\"col-sm-6\">\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction:<br>{{ instruction_text }}</h4>\n          </div>\n        </div>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n          <button type=\"submit\" class=\"btn btn-success\">Back to Search</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <form method=\"post\" action=\"{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}\">\n          <button type=\"submit\" class=\"btn btn-primary\">&lt; Prev</button>\n        </form>\n      </div>\n      <div class=\"row top-buffer\">\n        <div class=\"col-md-12\">\n          <div class=\"row top-buffer\">\n            <div class=\"col-sm-6\" name=\"reviews\">\n              <div class=\"card card-body\">\n                {% for review in product_info.Reviews %}\n                  <div class=\"card\">\n                    <div class=\"row text-left\">\n                      <h4 class=\"blue-text mt-3\">\"{{review.title}}\"</h4>\n                      <p class=\"text-left\">\n                        <span>{{review.score}}</span>\n                        {% for i in range(review.score | int) %}\n                            <span class=\"fa fa-star star-active\"></span>\n                        {% endfor %}\n                        {% for i in range(5 - review.score | int) %}\n                            <span class=\"fa fa-star star-inactive\"></span>\n                        {% endfor %}\n                      </p>\n                      <p class=\"content\">{{review.body}}</p>\n                    </div>\n                  </div>\n                {% endfor %}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/templates/search_page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"{{url_for('static', filename='style.css')}}\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\">\n    <link rel=\"icon\" href=\"data:,\">\n  </head>\n  <body>\n    <!-- Code reference: https://bootsnipp.com/snippets/m13mN -->\n    <div class=\"container\" style=\"margin-top: 8%;\">\n      <div class=\"col-md-6 col-md-offset-3\">     \n        <div class=\"row\">\n          <div id=\"logo\" class=\"text-center\">\n            <h2>WebShop</h2>\n          </div>\n          <div id=\"instruction-text\" class=\"text-center\">\n            <h4>Instruction: <br>{{ instruction_text }}</h4>\n          </div>\n          <form role=\"form\" id=\"form-buscar\" method=\"post\" action=\"{{url_for('index', session_id=session_id)}}\">\n            <div class=\"form-group\">\n              <div class=\"input-group\">\n                <input id=\"search_input\" class=\"form-control\" type=\"text\" name=\"search_query\" placeholder=\"Search...\" required/>\n                <span class=\"input-group-btn\">\n                  <button class=\"btn btn-success\" type=\"submit\"><i class=\"glyphicon glyphicon-search\" aria-hidden=\"true\"></i>Search</button>\n                </span>\n              </div>\n            </div>\n          </form>\n        </div>            \n      </div>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "envs/webshop/src/webshop/web_agent_site/utils.py",
    "content": "import os\nimport bisect\nimport hashlib\nimport logging\nimport random\nfrom os.path import dirname, abspath, join\n\nBASE_DIR = join(dirname(abspath(__file__)), '../..')\nDEBUG_PROD_SIZE = None  # set to `None` to disable\n\nDEFAULT_ATTR_PATH = join(BASE_DIR, '../data/items_ins_v2.json')\nDEFAULT_FILE_PATH = join(BASE_DIR, '../data/items_shuffle.json')\nDEFAULT_REVIEW_PATH = join(BASE_DIR, '../data/reviews.json')\n\nFEAT_CONV = join(BASE_DIR, '../data/feat_conv.pt')\nFEAT_IDS = join(BASE_DIR, '../data/feat_ids.pt')\n\nHUMAN_ATTR_PATH = join(BASE_DIR, '../data/items_human_ins.json')\n# HUMAN_ATTR_PATH = join(BASE_DIR, '../data/items_human_ins.json')\n\ndef random_idx(cum_weights):\n    \"\"\"Generate random index by sampling uniformly from sum of all weights, then\n    selecting the `min` between the position to keep the list sorted (via bisect)\n    and the value of the second to last index\n    \"\"\"\n    pos = random.uniform(0, cum_weights[-1])\n    idx = bisect.bisect(cum_weights, pos)\n    idx = min(idx, len(cum_weights) - 2)\n    return idx\n\ndef setup_logger(session_id, user_log_dir):\n    \"\"\"Creates a log file and logging object for the corresponding session ID\"\"\"\n    logger = logging.getLogger(session_id)\n    formatter = logging.Formatter('%(message)s')\n    file_handler = logging.FileHandler(\n        user_log_dir / f'{session_id}.jsonl',\n        mode='w'\n    )\n    file_handler.setFormatter(formatter)\n    logger.setLevel(logging.INFO)\n    logger.addHandler(file_handler)\n    return logger\n\ndef generate_mturk_code(session_id: str) -> str:\n    \"\"\"Generates a redeem code corresponding to the session ID for an MTurk\n    worker once the session is completed\n    \"\"\"\n    sha = hashlib.sha1(session_id.encode())\n    return sha.hexdigest()[:10].upper()"
  },
  {
    "path": "requirements.txt",
    "content": "openai==2.6.1\nrich==14.2.0\ntorch==2.9.0"
  },
  {
    "path": "run.py",
    "content": "import argparse\nimport asyncio\nimport importlib\nimport time\nimport shutil\nfrom pathlib import Path\nfrom typing import Type, Dict, Any, List, Optional\nimport inspect\n\nimport logging\nfrom datetime import datetime\n\n# Suppress common warnings - must be at the very beginning\nimport warnings\nimport os\nos.environ['PYTHONWARNINGS'] = 'ignore::DeprecationWarning'\n\n# Suppress all categories of warnings\nwarnings.simplefilter(\"ignore\")\nwarnings.filterwarnings(\"ignore\")\n\n# Make sure these are applied globally\nimport sys\nif not sys.warnoptions:\n    warnings.simplefilter(\"ignore\")\n\n# Specific warning suppressions\nwarnings.filterwarnings(\"ignore\", category=DeprecationWarning)\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\n\n# Module-specific suppressions\nwarnings.filterwarnings(\"ignore\", module=\"gym\")\nwarnings.filterwarnings(\"ignore\", module=\"gym.*\")\nwarnings.filterwarnings(\"ignore\", module=\"faiss\")\nwarnings.filterwarnings(\"ignore\", module=\"faiss.*\") \nwarnings.filterwarnings(\"ignore\", module=\"setuptools\")\nwarnings.filterwarnings(\"ignore\", module=\"setuptools.*\")\nwarnings.filterwarnings(\"ignore\", module=\"typer\")\nwarnings.filterwarnings(\"ignore\", module=\"typer.*\")\nwarnings.filterwarnings(\"ignore\", module=\"spacy\")\nwarnings.filterwarnings(\"ignore\", module=\"spacy.*\")\nwarnings.filterwarnings(\"ignore\", module=\"click\")\nwarnings.filterwarnings(\"ignore\", module=\"click.*\")\n\nfrom tqdm import tqdm\n\nfrom base.agent import Agent\nfrom base.environment import Env\nfrom utils.logger import SimpleLogger\nfrom utils.errors import StepLimitError\n\n# Allow short aliases like `-a human` and `-e alfworld`\nAGENT_ALIASES = {\n    \"recode\": \"agents.recode.agent.ReCodeAgent\",\n}\n\nENV_ALIASES = {\n    \"alfworld\": \"envs.alfworld.env.AlfworldEnv\",\n    \"webshop\": \"envs.webshop.env.WebShopEnv\",\n    \"sciworld\": \"envs.sciworld.env.SciWorldEnv\",\n}\n\ndef resolve_class_identifier(identifier: str, aliases: Dict[str, str], kind: str) -> str:\n    \"\"\"Resolve a possibly-short alias (e.g., 'human') to a full dotted class path.\n\n    If `identifier` already looks like a dotted path, return it unchanged.\n    Otherwise, look up a lowercase alias in `aliases`.\n    \"\"\"\n    if not identifier:\n        raise ValueError(f\"Empty {kind} identifier\")\n    if \".\" in identifier:\n        return identifier\n    key = identifier.strip().lower()\n    if key in aliases:\n        return aliases[key]\n    available = \", \".join(sorted(aliases.keys()))\n    raise ValueError(f\"Unknown {kind} alias '{identifier}'. Available: {available}\")\n\ndef _default_run_id(agent_path: str, env_path: str) -> str:\n    \"\"\"Generate default run_id = <timestamp>_<AgentCls>_<EnvCls>.\"\"\"\n    ts = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n    agent_cls_name = agent_path.split(\".\")[-1]\n    env_cls_name = env_path.split(\".\")[-1]\n    return f\"{ts}_{agent_cls_name}_{env_cls_name}\"\n\ndef create_instance(cls: Type, running_config: Optional[Dict[str, Any]], logger: Optional[SimpleLogger]):\n    \"\"\"Instantiate a class, injecting logger and config-defined constructor kwargs.\"\"\"\n    sig = inspect.signature(cls)\n    kwargs: Dict[str, Any] = {}\n\n    if logger is not None and \"logger\" in sig.parameters:\n        kwargs[\"logger\"] = logger\n\n    for k in running_config or {}:\n        if k in sig.parameters and k not in kwargs:\n            kwargs[k] = running_config[k]\n\n    if \"task_type\" in sig.parameters and running_config and \"task_types\" in running_config:\n        task_types = running_config.get(\"task_types\", [])\n        if isinstance(task_types, list) and task_types:\n            kwargs[\"task_type\"] = task_types[0].upper()\n        elif isinstance(task_types, str):\n            kwargs[\"task_type\"] = task_types.upper()\n\n    try:\n        return cls(**kwargs)  # type: ignore[arg-type]\n    except TypeError:\n        return cls()\n\n\ndef load_class(path: str) -> Type:\n    \"\"\"Import a class given a dotted path \"package.module.Class\" only.\"\"\"\n    try:\n        module_path, class_name = path.rsplit(\".\", 1)\n    except ValueError:\n        raise ValueError(f\"Invalid class path '{path}'. Expected format: package.module.Class\")\n\n    module = importlib.import_module(module_path)\n    cls = getattr(module, class_name, None)\n    if not isinstance(cls, type):\n        raise AttributeError(f\"'{path}' does not resolve to a class\")\n    return cls\n\n\ndef _safe_report(obj: Any) -> Dict[str, Any]:\n    \"\"\"Call obj.report() if available and return a dict; otherwise return {}.\"\"\"\n    try:\n        if hasattr(obj, \"report\") and callable(getattr(obj, \"report\")):\n            data = getattr(obj, \"report\")() or {}\n            return data if isinstance(data, dict) else {}\n    except Exception:\n        return {}\n    return {}\n\n\ndef _assemble_result(\n    agent: Agent,\n    env: Env,\n    instance_id: Optional[int],\n    duration: float,\n    error: Optional[str] = None,\n) -> Dict[str, Any]:\n    \"\"\"Assemble the unified result dict from agent/env reports plus local info.\"\"\"\n    agent_report = _safe_report(agent)\n    # print(f\"[HERE] {agent_report}\")\n    env_report = _safe_report(env)\n    # Ensure task_type present if env exposes it\n    if hasattr(env, \"task_type\") and \"task_type\" not in env_report:\n        try:\n            env_report[\"task_type\"] = getattr(env, \"task_type\")\n        except Exception:\n            pass\n    local_info: Dict[str, Any] = {\n        \"instance_id\": instance_id,\n        \"time\": duration,\n    }\n    if error is not None:\n        local_info[\"error\"] = error\n    return {**agent_report, **env_report, **local_info}\n\nasync def run_single_instance(\n    agent: Agent,\n    env: Env,\n    config: Dict[str, Any],\n    logger: SimpleLogger,\n    instance_id: Optional[int] = None,\n) -> Dict[str, Any]:\n    \"\"\"Run one episode and collect result dict (async).\"\"\"\n\n    # Determine per-instance time limit (seconds). Default to 900s if unspecified.\n    try:\n        max_duration_cfg = config.get(\"max_duration\", 900)\n        time_limit_secs = float(max_duration_cfg if max_duration_cfg is not None else 900)\n        if time_limit_secs <= 0:\n            time_limit_secs = 900.0\n    except Exception:\n        time_limit_secs = 900.0\n\n    init_info = env.reset(config, str(instance_id) if instance_id is not None else None)\n    observations = init_info[\"observations\"]\n    agent.reset(config, init_info)\n    logger.info(f\"[Instance {instance_id}] Environment reset. Starting episode.\")\n\n    start_time = time.time()\n\n    async def episode_runner() -> Dict[str, Any]:\n        nonlocal observations\n        try:\n            while not env.is_done():\n                actions = await agent.act(observations)\n                observations = await env.run(actions)\n\n            success = env.is_success()\n            duration_local = time.time() - start_time\n            final_steps_local = env._step_count\n\n            logger.info(\n                f\"{env.id}-Finished: {'SUCCESS' if success else 'FAILURE'} \"\n                f\"({final_steps_local} steps, {duration_local:.4f}s)\"\n            )\n\n            return _assemble_result(agent, env, instance_id, duration_local)\n\n        except StepLimitError as e:\n            duration_local = time.time() - start_time\n            final_steps_local = env._step_count\n            logger.warning(f\"[Instance {instance_id}] {e} ({final_steps_local} steps, {duration_local:.4f}s)\")\n\n            return _assemble_result(agent, env, instance_id, duration_local, error=str(e))\n\n        except Exception as e:\n            duration_local = time.time() - start_time\n            try:\n                final_steps_local = env.get_step_count()\n            except Exception:\n                final_steps_local = getattr(env, \"_step_count\", 0)\n            logger.error(f\"{env.id}-ERROR: {e} ({final_steps_local} steps, {duration_local:.4f}s)\")\n\n            return _assemble_result(agent, env, instance_id, duration_local, error=str(e))\n\n    try:\n        result = await asyncio.wait_for(episode_runner(), timeout=time_limit_secs)\n        return result\n    except asyncio.TimeoutError:\n        duration = time.time() - start_time\n        try:\n            final_steps = env.get_step_count()\n        except Exception:\n            final_steps = getattr(env, \"_step_count\", 0)\n        logger.warning(\n            f\"[Instance {instance_id}] TIMEOUT after {int(time_limit_secs)}s \"\n            f\"({final_steps} steps, {duration:.4f}s)\"\n        )\n        res = _assemble_result(\n            agent, env, instance_id, duration, error=f\"Timeout after {int(time_limit_secs)}s\"\n        )\n        # Explicitly mark as failure to ensure correct final statistics\n        res[\"success\"] = False\n        return res\n\n\nasync def run_concurrent_instances(\n    agent_cls: Type[Agent],\n    env_cls: Type[Env],\n    num_instances: int,\n    max_concurrent: int = 10,\n    config: Optional[Dict[str, Any]] = None,\n    logger: Optional[SimpleLogger] = None,\n) -> List[Dict[str, Any]]:\n    \"\"\"Run many environment instances concurrently with a live progress UI.\n\n    If `rich` is available, use a richer UI with per-instance spinners; otherwise\n    fallback to tqdm-based overall bar plus lightweight per-instance lines.\n    \"\"\"\n\n    config = config or {}\n    # Determine base start id from running_config\n    try:\n        base_start_id = int(config.get(\"start_id\", 0) or 0)\n    except (TypeError, ValueError):\n        base_start_id = 0\n\n    sem = asyncio.Semaphore(max_concurrent)\n\n    # Decide whether to use any progress UI. Allow config to forcibly disable it\n    # (e.g., for HumanAgent which reads from stdin and conflicts with live updating UIs).\n    disable_rich_ui = False\n    try:\n        # Accept multiple possible keys to disable rich UI\n        for key in (\"disable_rich_ui\", \"no_rich\", \"disable_rich\"):\n            v = config.get(key)\n            if isinstance(v, str):\n                v_norm = v.strip().lower()\n                if v_norm in (\"1\", \"true\", \"yes\", \"y\", \"on\"):  # treat truthy strings as True\n                    disable_rich_ui = True\n                    break\n            elif v:\n                disable_rich_ui = True\n                break\n    except Exception:\n        disable_rich_ui = False\n\n    # Also disable UI if HumanAgent is used to avoid interfering with stdin\n    try:\n        if getattr(agent_cls, \"__name__\", \"\") == \"HumanAgent\":\n            disable_rich_ui = True\n    except Exception:\n        pass\n\n    use_rich = False\n    if not disable_rich_ui:\n        try:\n            from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, BarColumn, TaskProgressColumn\n            from rich.console import Group\n            from rich.live import Live\n            from rich.text import Text\n            use_rich = True\n        except Exception:\n            use_rich = False\n\n    # Common runner utilities -------------------------------------------------\n    def make_instance_logger(effective_id: int):\n        instance_logger = None\n        if logger is not None:\n            import logging\n            instance_logger_name = f\"instance_{effective_id}_{logger.run_id}\"\n            instance_logger_obj = logging.getLogger(instance_logger_name)\n            instance_logger_obj.setLevel(logging.INFO)\n            instance_logger_obj.handlers.clear()\n            instance_log_file = Path(logger.get_log_dir()) / f\"instance_{effective_id}.log\"\n            file_handler = logging.FileHandler(instance_log_file, mode=\"w\", encoding=\"utf-8\")\n            from utils.logger import MultiLineFormatter\n            file_handler.setFormatter(MultiLineFormatter('%(asctime)s - %(levelname)s - %(message)s'))\n            instance_logger_obj.addHandler(file_handler)\n\n            class InstanceLogger:\n                def __init__(self, logger_obj, main_logger):\n                    self.logger = logger_obj\n                    self.main_logger = main_logger\n                    self.run_id = main_logger.run_id\n                def info(self, message):\n                    self.logger.info(message)\n                def warning(self, message):\n                    self.logger.warning(message)\n                def error(self, message):\n                    self.logger.error(message)\n                def get_log_dir(self):\n                    return self.main_logger.get_log_dir()\n                def get_base_dir(self):\n                    return self.main_logger.get_base_dir()\n\n            instance_logger = InstanceLogger(instance_logger_obj, logger)\n        return instance_logger or logger\n\n    # No-UI branch (for HumanAgent or when explicitly disabled) --------------\n    if disable_rich_ui:\n        results: List[Dict[str, Any]] = []\n\n        for instance_id in range(num_instances):\n            effective_id = base_start_id + instance_id\n            plogger = make_instance_logger(effective_id)\n            agent = create_instance(agent_cls, config, plogger)\n            env = create_instance(env_cls, config, plogger)\n            res = await run_single_instance(agent, env, config, plogger, effective_id)\n            results.append(res)\n\n        return results\n\n    # Rich UI branch ----------------------------------------------------------\n    if use_rich:\n        results: List[Dict[str, Any]] = []\n\n        overall_progress = Progress(\n            TextColumn(\"[bold]Overall[/bold]\"),\n            BarColumn(bar_width=None),\n            TaskProgressColumn(),\n            TimeElapsedColumn(),\n            refresh_per_second=8,\n        )\n\n        instances_progress = Progress(\n            SpinnerColumn(style=\"cyan\"),\n            TextColumn(\"[bold]{task.description}[/bold]\"),\n            TextColumn(\"{task.fields[status]}\", style=\"dim\"),\n            refresh_per_second=8,\n        )\n\n        instance_tasks: Dict[int, int] = {}\n        finished_names: List[str] = []\n\n        async def runner(instance_id: int):\n            async with sem:\n                effective_id = base_start_id + instance_id\n                plogger = make_instance_logger(effective_id)\n                agent = create_instance(agent_cls, config, plogger)\n                env = create_instance(env_cls, config, plogger)\n\n                # Add a per-instance spinner task\n                task_id = instances_progress.add_task(f\"instance {effective_id}\", status=\"running\")\n                instance_tasks[effective_id] = task_id\n                try:\n                    res = await run_single_instance(agent, env, config, plogger, effective_id)\n                    overall_progress.update(overall_task, advance=1)\n                    # Remove finished task from running list and update Done line\n                    try:\n                        # Prefer hiding the task to avoid accumulating many visible lines\n                        instances_progress.update(task_id, status=\"done\", visible=True)\n                        # Immediately hide the finished task for a clean UI\n                        instances_progress.update(task_id, visible=False)\n                    except Exception:\n                        pass\n                    try:\n                        # Best-effort removal (not strictly required if hidden)\n                        instances_progress.remove_task(task_id)\n                    except Exception:\n                        pass\n                    finished_names.append(f\"instance {effective_id}\")\n                    try:\n                        done_renderable = Text(\"✔ Done: \", style=\"green\")\n                        if finished_names:\n                            done_renderable.append(\", \".join(finished_names))\n                        live.update(Group(overall_progress, instances_progress, done_renderable))\n                    except Exception:\n                        pass\n                    return res\n                finally:\n                    # Keep finished tasks displayed; just cleanup mapping\n                    instance_tasks.pop(effective_id, None)\n                    # Ensure any lingering task is hidden in case of earlier failure\n                    try:\n                        tid = instance_tasks.get(effective_id)\n                        if tid is not None:\n                            instances_progress.update(tid, visible=False)\n                    except Exception:\n                        pass\n\n        with Live(Group(overall_progress, instances_progress, Text(\"✔ Done: \", style=\"green\")), refresh_per_second=8, transient=False) as live:\n            overall_task = overall_progress.add_task(\"Instances\", total=num_instances)\n            tasks = [asyncio.create_task(runner(i)) for i in range(num_instances)]\n            raw_results = await asyncio.gather(*tasks, return_exceptions=True)\n\n        for idx, res in enumerate(raw_results):\n            if isinstance(res, Exception):\n                if logger:\n                    logger.error(f\"Instance {idx} raised exception: {res}\")\n                else:\n                    print(f\"Instance {idx} raised exception: {res}\")\n                results.append({\"instance_id\": idx, \"success\": False, \"error\": str(res)})\n            else:\n                results.append(res)  # type: ignore[arg-type]\n\n        return results\n\n    # Fallback tqdm branch ----------------------------------------------------\n    # Main overall progress bar (position 0)\n    progress_bar = tqdm(total=num_instances, desc=\"Instances\", leave=True)\n\n    # Allocate fixed display slots for per-instance lightweight spinners\n    slot_queue: asyncio.Queue[int] = asyncio.Queue()\n    for i in range(max_concurrent):\n        slot_queue.put_nowait(i)\n\n    slot_bars = [\n        tqdm(\n            total=1,\n            position=1 + i,\n            leave=True,\n            bar_format=\"{desc} {postfix}\",\n            dynamic_ncols=True,\n        )\n        for i in range(max_concurrent)\n    ]\n    for i, bar in enumerate(slot_bars):\n        bar.set_description_str(\"[instance -]\")\n        bar.set_postfix_str(\"\")\n\n    active_slots: Dict[int, Dict[str, Any]] = {}\n    stop_spinners = asyncio.Event()\n\n    async def spinner_updater():\n        spinner_chars = [\"|\", \"/\", \"-\", \"\\\\\"]\n        idx = 0\n        try:\n            while not stop_spinners.is_set():\n                for slot, meta in list(active_slots.items()):\n                    bar = slot_bars[slot]\n                    inst_id = meta.get(\"id\")\n                    bar.set_description_str(f\"[instance {inst_id}]\")\n                    bar.set_postfix_str(f\"running {spinner_chars[idx % len(spinner_chars)]}\")\n                    bar.refresh()\n                idx += 1\n                await asyncio.sleep(0.1)\n        finally:\n            for slot, meta in list(active_slots.items()):\n                bar = slot_bars[slot]\n                inst_id = meta.get(\"id\")\n                bar.set_description_str(f\"[instance {inst_id}]\")\n                bar.set_postfix_str(\"done\")\n                bar.refresh()\n\n    async def runner(instance_id: int):\n        async with sem:\n            effective_id = base_start_id + instance_id\n            slot = await slot_queue.get()\n            active_slots[slot] = {\"id\": effective_id}\n\n            plogger = make_instance_logger(effective_id)\n            agent = create_instance(agent_cls, config, plogger)\n            env = create_instance(env_cls, config, plogger)\n            try:\n                result = await run_single_instance(agent, env, config, plogger, effective_id)\n                progress_bar.update(1)\n                return result\n            finally:\n                try:\n                    bar = slot_bars[slot]\n                    bar.set_description_str(f\"[instance {effective_id}]\")\n                    bar.set_postfix_str(\"done\")\n                    bar.refresh()\n                except Exception:\n                    pass\n                active_slots.pop(slot, None)\n                slot_queue.put_nowait(slot)\n\n    spinner_task = asyncio.create_task(spinner_updater())\n    tasks = [asyncio.create_task(runner(i)) for i in range(num_instances)]\n    raw_results = await asyncio.gather(*tasks, return_exceptions=True)\n\n    stop_spinners.set()\n    try:\n        await spinner_task\n    except Exception:\n        pass\n    progress_bar.close()\n    for bar in slot_bars:\n        try:\n            bar.close()\n        except Exception:\n            pass\n\n    results: List[Dict[str, Any]] = []\n    for idx, res in enumerate(raw_results):\n        if isinstance(res, Exception):\n            if logger:\n                logger.error(f\"Instance {idx} raised exception: {res}\")\n            else:\n                print(f\"Instance {idx} raised exception: {res}\")\n            results.append({\"instance_id\": idx, \"success\": False, \"error\": str(res)})\n        else:\n            results.append(res)  # type: ignore[arg-type]\n\n    return results\n\n\ndef write_summary(results: List[Dict[str, Any]], output_file: Path):\n    \"\"\"Write results summary to `output_file`, creating parent dirs.\"\"\"\n\n    total = len(results)\n    successes = sum(1 for r in results if r.get(\"success\"))\n    # Per-task-type aggregation (only if task_type present)\n    by_task: Dict[str, Dict[str, Any]] = {}\n    for r in results:\n        if \"task_type\" not in r or r.get(\"task_type\") is None:\n            continue\n        task_type = str(r.get(\"task_type\"))\n        bucket = by_task.setdefault(task_type, {\n            \"total_instances\": 0,\n            \"successful_instances\": 0,\n            \"total_time\": 0.0,\n            \"total_steps\": 0,\n            \"total_cost\": 0.0,\n            \"total_reward\": 0.0,\n        })\n        bucket[\"total_instances\"] += 1\n        if r.get(\"success\"):\n            bucket[\"successful_instances\"] += 1\n        bucket[\"total_time\"] += float(r.get(\"time\", 0.0))\n        bucket[\"total_steps\"] += int(r.get(\"steps\", 0) or 0)\n        bucket[\"total_cost\"] += float(r.get(\"cost\", 0.0))\n        bucket[\"total_reward\"] += float(r.get(\"reward\", 0.0))\n    # Compute averages per task\n    for t, b in by_task.items():\n        ti = b[\"total_instances\"] or 1\n        b[\"success_rate\"] = b[\"successful_instances\"] / ti\n        b[\"avg_time_per_instance\"] = b[\"total_time\"] / ti\n        b[\"avg_steps_per_instance\"] = b[\"total_steps\"] / ti\n        b[\"avg_cost_per_instance\"] = b[\"total_cost\"] / ti\n\n    # Dynamically aggregate all numeric-like metrics (totals and averages)\n    # Exclude only 'success' to avoid double counting in metrics,\n    # and exclude non-meaningful fields like instance_id\n    numeric_keys = set()\n    for r in results:\n        for k, v in r.items():\n            if k in (\"instance_id\", \"success\"):\n                continue\n            if isinstance(v, (int, float, bool)):\n                numeric_keys.add(k)\n\n    metrics_total: Dict[str, float] = {}\n    metrics_avg: Dict[str, float] = {}\n    for k in sorted(numeric_keys):\n        s = 0.0\n        for r in results:\n            try:\n                val = r.get(k, 0)\n                s += float(val or 0)\n            except Exception:\n                continue\n        metrics_total[k] = s\n        metrics_avg[k] = (s / total) if total > 0 else 0.0\n\n    # Per-task dynamic metrics\n    by_task_metrics: Dict[str, Dict[str, Dict[str, float]]] = {}\n    if by_task:\n        for task_type in by_task.keys():\n            totals: Dict[str, float] = {}\n            avgs: Dict[str, float] = {}\n            bucket_results = [r for r in results if str(r.get(\"task_type\")) == task_type]\n            bucket_n = len(bucket_results) or 1\n            for k in sorted(numeric_keys):\n                s = 0.0\n                for r in bucket_results:\n                    try:\n                        val = r.get(k, 0)\n                        s += float(val or 0)\n                    except Exception:\n                        continue\n                totals[k] = s\n                avgs[k] = s / bucket_n\n            by_task_metrics[task_type] = {\"metrics_total\": totals, \"metrics_avg\": avgs}\n\n    summary = {\n        \"summary\": {\n            \"total_instances\": total,\n            \"successful_instances\": successes,\n            \"success_rate\": successes / total if total > 0 else 0,\n            \"metrics_total\": metrics_total,\n            \"metrics_avg\": metrics_avg,\n        },\n        \"instances\": results,\n    }\n    if by_task:\n        # merge base by_task stats with dynamic metrics\n        merged_by_task: Dict[str, Any] = {}\n        for t, base_stats in by_task.items():\n            merged = dict(base_stats)\n            if t in by_task_metrics:\n                merged.update(by_task_metrics[t])\n            merged_by_task[t] = merged\n        summary[\"by_task_type\"] = merged_by_task\n\n    output_file.parent.mkdir(parents=True, exist_ok=True)\n    import json\n    output_file.write_text(json.dumps(summary, indent=2))\n\n    print(\"\\n📊 Summary:\")\n    rate_pct = (successes / total * 100.0) if total > 0 else 0.0\n    print(f\"   Success: {successes}/{total} ({rate_pct:.4f}%)\")\n    print(f\"   Results saved to: {output_file}\")\n    # Print per-task breakdown if any\n    if by_task:\n        print(\"   By task_type:\")\n        for t, b in by_task.items():\n            rpct = b[\"success_rate\"] * 100.0\n            print(f\"     - {t}: {b['successful_instances']}/{b['total_instances']} ({rpct:.4f}%)\")\n    # Print standard metrics if present\n    standard_keys_order = [\"time\", \"steps\", \"cost\", \"reward\"]\n    std_present = [k for k in standard_keys_order if k in metrics_total]\n    if std_present:\n        print(\"   Metrics (totals/avg):\")\n        for k in std_present:\n            total_v = metrics_total[k]\n            avg_v = metrics_avg[k]\n            try:\n                print(f\"     - {k}: total={total_v:.4f}, avg={avg_v:.4f}\")\n            except Exception:\n                print(f\"     - {k}: total={total_v}, avg={avg_v}\")\n    # Print any additional numeric metrics not already shown (excluding 'success')\n    excluded_keys = set(std_present)\n    extra_keys = [k for k in metrics_total.keys() if k not in excluded_keys]\n    if extra_keys:\n        print(\"   Extra metrics (totals/avg):\")\n        for k in extra_keys:\n            total_v = metrics_total[k]\n            avg_v = metrics_avg[k]\n            try:\n                print(f\"     - {k}: total={total_v:.4f}, avg={avg_v:.4f}\")\n            except Exception:\n                print(f\"     - {k}: total={total_v}, avg={avg_v}\")\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Run an agent in an environment\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n    )\n    parser.add_argument(\n        \"-a\", \"--agent\",\n        type=str,\n        default=\"agents.recode.agent.ReCodeAgent\",\n        help=\"Agent class path or alias. Examples: agents.recode.agent.ReCodeAgent | aliases: human, recode, react, codeact, adaplanner\",\n    )\n    parser.add_argument(\n        \"-e\", \"--env\",\n        type=str,\n        default=\"envs.alfworld.env.AlfworldEnv\",\n        help=\"Environment class path or alias. Examples: envs.alfworld.env.AlfworldEnv | aliases: alfworld, webshop, sciworld, travelplanner\",\n    )\n    parser.add_argument(\n        \"-n\", \"--instances\",\n        type=int,\n        default=1,\n        help=\"Number of instances to run\",\n    )\n    parser.add_argument(\n        \"-c\", \"--concurrent\",\n        type=int,\n        default=1,\n        help=\"Maximum concurrent instances\",\n    )\n    parser.add_argument(\n        \"-o\", \"--output\",\n        type=str,\n        default=\"results.json\",\n        help=\"Results JSON filename (will be saved in logs/<log_dir>/)\",\n    )\n    parser.add_argument(\n        \"-C\", \"--config\",\n        type=str,\n        default=None,\n        help=\"YAML config file path. Values here override CLI flags.\",\n    )\n    parser.add_argument(\n        \"--split\",\n        type=str,\n        default=\"test\",\n        help=\"Dataset split to use (e.g., train/valid/test)\",\n    )\n    parser.add_argument(\n        \"--seed\",\n        type=int,\n        default=42,\n        help=\"Random seed forwarded to environments\",\n    )\n    parser.add_argument(\n        \"-p\", \"--profile\",\n        type=str,\n        default=None,\n        help=\"LLM profile name forwarded to the agent\",\n    )\n    parser.add_argument(\n        \"-l\", \"--log-dir\",\n        type=str,\n        default=None,\n        help=\"Custom log directory name (otherwise autogenerated)\",\n    )\n    parser.add_argument(\n        \"--max-depth\",\n        type=int,\n        default=None,\n        help=\"Maximum depth for agent execution\",\n    )\n\n    args = parser.parse_args()\n\n    try:\n        # Load YAML config (overrides CLI)\n        import yaml\n        yaml_cfg = {}\n        if args.config:\n            try:\n                with open(args.config) as f:\n                    yaml_cfg = yaml.safe_load(f) or {}\n            except FileNotFoundError:\n                print(f\"⚠️  Config file not found: {args.config}. Using CLI values only.\")\n                yaml_cfg = {}\n\n        # Compose final config: CLI base, YAML overrides\n        cli_cfg: Dict[str, Any] = {\n            \"agent\": args.agent,\n            \"env\": args.env,\n            \"instances\": args.instances,\n            \"concurrent\": args.concurrent,\n            \"output\": args.output,\n            \"log_dir\": args.log_dir,\n            \"split\": args.split,\n            \"seed\": args.seed,\n            \"profile\": args.profile,\n            \"max_depth\": args.max_depth,\n        }\n        config: Dict[str, Any] = {**cli_cfg, **yaml_cfg}\n\n        agent_path: str = config.get(\"agent\", args.agent)\n        env_path: str = config.get(\"env\", args.env)\n        instances: int = int(config.get(\"instances\", args.instances) or 1)\n        concurrent: int = int(config.get(\"concurrent\", args.concurrent) or 1)\n        output_name: str = str(config.get(\"output\", args.output))\n\n        # Resolve short aliases if provided\n        agent_path = resolve_class_identifier(agent_path, AGENT_ALIASES, \"agent\")\n        env_path = resolve_class_identifier(env_path, ENV_ALIASES, \"env\")\n\n        agent_cls = load_class(agent_path)\n        env_cls = load_class(env_path)\n\n        # Use class names for default run_id for readability\n        run_id = config.get(\"log_dir\") or _default_run_id(agent_cls.__name__, env_cls.__name__)\n        # Clear existing log directory if present\n        existing_base_dir = Path(\"logs\") / run_id\n        if existing_base_dir.exists():\n            try:\n                shutil.rmtree(existing_base_dir)\n            except Exception as e:\n                print(f\"⚠️  Failed to clear existing log directory: {existing_base_dir} ({e})\")\n        logger = SimpleLogger(run_id=run_id)\n\n        # Special handling for HumanAgent: disable Rich UI and force concurrency to 1\n        is_human_agent = (getattr(agent_cls, \"__name__\", \"\") == \"HumanAgent\") or agent_path.endswith(\".HumanAgent\")\n        if is_human_agent:\n            if concurrent != 1:\n                logger.info(f\"Human agent detected. Forcing max concurrent to 1 (was {concurrent}).\")\n            concurrent = 1\n            config[\"concurrent\"] = 1\n            config[\"disable_rich_ui\"] = True\n\n        logger.info(f\"🤖 Agent: {agent_path}\")\n        logger.info(f\"🌍 Environment: {env_path}\")\n        logger.info(f\"📊 Instances: {instances} (max {concurrent} concurrent)\")\n        logger.info(\"-\" * 50)\n\n        results = asyncio.run(\n            run_concurrent_instances(agent_cls, env_cls, instances, concurrent, config, logger)\n        )\n\n        output_file = logger.get_base_dir() / output_name\n        write_summary(results, output_file)\n\n    except KeyboardInterrupt:\n        print(\"\\n⏹️  Interrupted by user\")\n    except Exception as e:\n        # import traceback\n        # traceback.print_exc()\n        print(f\"\\n❌ Error: {e}\")\n        return 1\n    \n    return 0\n\n\nif __name__ == \"__main__\":\n    exit(main()) "
  },
  {
    "path": "utils/common.py",
    "content": "import json\nimport yaml\nfrom pathlib import Path\nfrom typing import Any, List, Dict, Optional\nfrom pydantic_core import to_jsonable_python\nimport re\n\n\ndef read_json_file(json_file: str, encoding=\"utf-8\") -> List[Any]:\n    if not Path(json_file).exists():\n        raise FileNotFoundError(f\"json_file: {json_file} not exist, return []\")\n\n    with open(json_file, \"r\", encoding=encoding) as fin:\n        try:\n            data = json.load(fin)\n        except Exception:\n            raise ValueError(f\"read json file: {json_file} failed\")\n    return data\n\ndef write_json_file(json_file: str, data: list, encoding: str = None, indent: int = 4):\n    folder_path = Path(json_file).parent\n    if not folder_path.exists():\n        folder_path.mkdir(parents=True, exist_ok=True)\n\n    with open(json_file, \"w\", encoding=encoding) as fout:\n        json.dump(data, fout, ensure_ascii=False, indent=indent, default=to_jsonable_python)\n\ndef read_yaml_file(yaml_file: str, encoding='utf-8') -> Dict[str, Any]:\n    if not Path(yaml_file).exists():\n        raise FileNotFoundError(f\"yaml_file: {yaml_file} not exist, return empty dict\")\n    \n    with open(yaml_file, \"r\", encoding=encoding) as f:\n        try:\n            data = yaml.safe_load(f)\n        except Exception:\n            raise ValueError(f\"read yaml file: {yaml_file} failed\")\n    return data\n    \ndef parse_code_block(text: str, lang: str = \"python\") -> Optional[str]:\n    \"\"\"Extracts the first code block of a given language from a markdown-formatted text.\"\"\"\n    pattern = rf\"```{lang}\\s*\\n(.*?)\\n```\"\n    match = re.search(pattern, text, re.DOTALL)\n    if match:\n        return match.group(1).strip()\n    return None\n\ndef parse_xml_tag(response: str, xml_tag: str) -> str:\n    pattern = rf\"<{xml_tag}>(.*?)</{xml_tag}>\"\n    match = re.search(pattern, response, re.DOTALL)\n    return match.group(1).strip() if match else \"\""
  },
  {
    "path": "utils/errors.py",
    "content": "class StepLimitError(Exception):\n    \"\"\"Raised when the environment exceeds the maximum allowed step count.\"\"\"\n    pass "
  },
  {
    "path": "utils/executor.py",
    "content": "from typing import List, Dict, Any, Callable\nimport io, sys\nimport functools\nimport asyncio\nimport types\nimport re\nimport threading\nimport time\n\nfrom utils.llm import AsyncLLM\nfrom base.environment import Env\n\ndef print_output(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        result = func(*args, **kwargs)\n        if result is not None:\n            print(result, file=sys.stdout, flush=True)\n        return result\n    return wrapper\n\nclass Executor:\n    def __init__(self, env: Env = None, if_run_print: bool = False) -> None:\n        self.env = env\n        self.actions: List[str] = []\n        self._variables: Dict[str, Any] = {}\n        self.if_run_print = if_run_print\n        if self.if_run_print:\n            self.run = print_output(self.run)\n        self._base_globals = {\n            \"run\": self.run,\n            \"re\": re,\n        }\n        self._loop = None\n        self._loop_thread = None\n        self._start_loop_thread()\n\n    def register_function(self, name: str, func: Callable):\n        self._base_globals[name] = func\n    \n    def register_action_function(self, name: str, func: Callable):\n        func_with_run = lambda *args, **kwargs: self.run(func(*args, **kwargs))\n        self.register_function(name, func_with_run)\n\n    def register_ask_llm(self, llm: AsyncLLM):\n            def _ask_llm_sync(query: str) -> str:\n                async def _ask_llm(query: str) -> str:\n                    response, _cost = await llm(\n                        prompt=query,\n                    )\n                    return response\n                return self._submit_coro(_ask_llm(query))\n            self.register_function(\"ask_llm\", _ask_llm_sync)\n\n\n    def skip(self, reason: str):\n        return None\n    \n    def set_var(self, key: str, value: Any):\n        self._variables[key] = value\n    \n    def get_var(self, key: str) -> Any:\n        if key not in self._variables:\n            return None\n        return self._variables.get(key)\n    \n    def set_env(self, env: Env):\n        self.env = env\n    \n    def _is_preserved_variable(self, key: str, value: Any) -> bool:\n        if key.startswith('_') or key in self._base_globals:\n            return False\n        return not isinstance(value, (types.ModuleType, types.FunctionType, \n                                    types.BuiltinFunctionType, types.MethodType, type))\n    \n    def _infer_type_string(self, value: Any, depth: int = 0, max_depth: int = 2) -> str:\n        if value is None:\n            return \"NoneType\"\n        if depth > max_depth:\n            return type(value).__name__\n        try:\n            if isinstance(value, (bool, int, float, str)):\n                return type(value).__name__\n            if isinstance(value, list):\n                if not value:\n                    return \"list\"\n                elem_types = {self._infer_type_string(v, depth + 1, max_depth) for v in value[:5]}\n                if len(elem_types) == 1:\n                    return f\"list[{next(iter(elem_types))}]\"\n                return \"list\"\n            if isinstance(value, tuple):\n                if not value:\n                    return \"tuple\"\n                elem_types = [self._infer_type_string(v, depth + 1, max_depth) for v in value[:5]]\n                if all(t == elem_types[0] for t in elem_types):\n                    return f\"tuple[{elem_types[0]}]\"\n                return f\"tuple[{', '.join(elem_types)}]\"\n            if isinstance(value, set):\n                if not value:\n                    return \"set\"\n                sample = list(value)[:5]\n                elem_types = {self._infer_type_string(v, depth + 1, max_depth) for v in sample}\n                if len(elem_types) == 1:\n                    return f\"set[{next(iter(elem_types))}]\"\n                return \"set\"\n            if isinstance(value, dict):\n                if not value:\n                    return \"dict\"\n                items = list(value.items())[:5]\n                key_types = {self._infer_type_string(k, depth + 1, max_depth) for k, _ in items}\n                val_types = {self._infer_type_string(v, depth + 1, max_depth) for _, v in items}\n                if len(key_types) == 1 and len(val_types) == 1:\n                    return f\"dict[{next(iter(key_types))}, {next(iter(val_types))}]\"\n                return \"dict\"\n            return type(value).__name__\n        except Exception:\n            return type(value).__name__\n    \n    def run(self, action: str) -> str:\n        if self.env is None:\n            raise RuntimeError(\"Environment not set. Call set_env() first.\")\n        \n        result = self._submit_coro(self.env.run(action))\n        \n        self.actions.append(action)\n        if isinstance(result, list):\n            result = \"\\n\".join(result)\n        return result\n    \n    def get_actions(self) -> List[str]:\n        actions = self.actions.copy()\n        self.actions.clear()\n        return actions\n    \n    def get_variables(self) -> str:\n        return \"\\n\".join([f\"- {key} ({self._infer_type_string(value)}): {value}\" for key, value in self._variables.items()])\n    \n    def reset(self):\n        self.actions.clear()\n        self._variables.clear()\n\n    def _start_loop_thread(self):\n        if self._loop and self._loop.is_running():\n            return\n        def _loop_runner():\n            loop = asyncio.new_event_loop()\n            self._loop = loop\n            asyncio.set_event_loop(loop)\n            loop.run_forever()\n        t = threading.Thread(target=_loop_runner, daemon=True)\n        t.start()\n        while self._loop is None or not self._loop.is_running():\n            time.sleep(0.01)\n        self._loop_thread = t\n\n    def _submit_coro(self, coro):\n        self._start_loop_thread()\n        future = asyncio.run_coroutine_threadsafe(coro, self._loop)\n        return future.result()\n\n    def close(self):\n        if self._loop and self._loop.is_running():\n            try:\n                self._loop.call_soon_threadsafe(self._loop.stop)\n            except Exception:\n                pass\n            if self._loop_thread:\n                self._loop_thread.join(timeout=1)\n        self._loop = None\n        self._loop_thread = None\n\n    def execute(self, code: str) -> Dict[str, Any]:\n        success, stdout_lines, error_msg = self._run_block(code)\n        return {\"code\": code, \"stdout\": stdout_lines, \"error\": error_msg, \"success\": success}\n\n    def _run_block(self, block: str) -> tuple[bool, List[str], str]:\n        output = []\n        \n        class OutputCapture:\n            def __init__(self):\n                self.lines = []\n            def write(self, text):\n                if text and text != '\\n':\n                    self.lines.extend(line for line in text.splitlines() if line.strip())\n            def flush(self): pass\n\n        capture = OutputCapture()\n        old_stdout = sys.stdout\n        sys.stdout = capture\n        \n        exec_globals = {**self._base_globals, **self._variables}\n        \n        try:\n            exec(block, exec_globals)\n            \n            for key, value in exec_globals.items():\n                if self._is_preserved_variable(key, value):\n                    self._variables[key] = value\n            \n            return True, capture.lines, \"\"\n        except NameError as e:\n            match = re.search(r\"name '(.+?)' is not defined\", str(e))\n            if match and f\"{match.group(1)}(\" in block:\n                return False, capture.lines, f\"NeedExpansion: `{match.group(1)}` needs to be expanded.\"\n            return False, capture.lines, f\"NameError: {e}\"\n        except Exception as e:\n            return False, capture.lines, f\"{e.__class__.__name__}: {e}\"\n        finally:\n            sys.stdout = old_stdout"
  },
  {
    "path": "utils/llm.py",
    "content": "import os\nimport asyncio\nimport yaml\nimport random\nfrom pathlib import Path\nfrom typing import Optional, Dict, Any, Tuple, Union, List\nfrom openai import AsyncOpenAI, APIError, APIConnectionError, APITimeoutError, RateLimitError\nfrom pydantic import BaseModel, Field, model_validator, ConfigDict\n\nfrom utils.common import read_json_file\n\nDEFAULT_LLM_PROFILE_PATH = Path(\"configs/profiles.yaml\")\nDEFAULT_PRICE_PATH = Path(\"configs/prices.json\")\n\nclass LLMConfig(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\", validate_default=True)\n    \n    api_key: Optional[str] = Field(\n        default=None,\n        description=\"OpenAI API key (defaults to OPENAI_API_KEY environment variable)\"\n    )\n    base_url: Optional[str] = Field(\n        default=None,\n        description=\"Custom API base URL for OpenAI-compatible endpoints\"\n    )\n    model: str = Field(default=\"gpt-4o-mini\", description=\"Model name to use\")\n    temperature: Optional[float] = Field(\n        default=None,\n        ge=0.0,\n        le=2.0,\n        description=\"Sampling temperature (omit by default; excluded for o-series)\"\n    )\n    max_tokens: Optional[int] = Field(\n        default=None,\n        gt=0,\n        description=\"Maximum number of tokens to generate (omit by default)\"\n    )\n    timeout: int = Field(default=60, gt=0, description=\"API request timeout in seconds\")\n    max_retries: int = Field(default=3, ge=0, description=\"Maximum retry attempts\")\n    retry_base_delay: float = Field(default=1.0, description=\"Base retry delay in seconds\")\n    retry_jitter: float = Field(default=0.1, description=\"Retry delay jitter factor\")\n    track_costs: bool = Field(default=True, description=\"Enable cost tracking\")\n\n    @classmethod\n    def from_profile(cls, profile: str = \"default\", config_path: Path = DEFAULT_LLM_PROFILE_PATH) -> \"LLMConfig\":\n        try:\n            with config_path.open(\"r\", encoding=\"utf-8\") as f:\n                config = yaml.safe_load(f)\n            \n            profile_config = config.get(\"models\", {}).get(profile, {})\n            \n            return cls(**{\n                k: v for k, v in profile_config.items()\n                if v is not None and k in cls.model_fields\n            })\n            \n        except Exception as e:\n            return cls()\n\n    @model_validator(mode=\"after\")\n    def resolve_api_key(self) -> \"LLMConfig\":\n        if not self.api_key:\n            self.api_key = os.environ.get(\"OPENAI_API_KEY\")\n        return self\n\nclass CostCalculator(BaseModel):\n    pricing: Dict[str, Dict[str, float]] = Field(\n        default_factory=lambda: read_json_file(DEFAULT_PRICE_PATH),\n        description=\"Pricing data in USD per million tokens\"\n    )\n    \n    def compute_cost(\n        self,\n        model: str,\n        prompt_tokens: int,\n        completion_tokens: int\n    ) -> Tuple[float, Dict[str, Any]]:\n        rates = self.pricing.get(model, self.pricing[\"default\"])\n        \n        input_cost = (prompt_tokens / 1e6) * rates[\"input\"]\n        output_cost = (completion_tokens / 1e6) * rates[\"output\"]\n        total_cost = input_cost + output_cost\n        \n        cost_breakdown = {\n            \"model\": model,\n            \"prompt_tokens\": prompt_tokens,\n            \"completion_tokens\": completion_tokens,\n            \"total_tokens\": prompt_tokens + completion_tokens,\n            \"input_cost\": input_cost,\n            \"output_cost\": output_cost,\n            \"total_cost\": total_cost,\n            \"currency\": \"USD\"\n        }\n        \n        return total_cost, cost_breakdown\n\nclass AsyncLLM(BaseModel):\n    config: LLMConfig = Field(default_factory=LLMConfig)\n    cost_calculator: CostCalculator = Field(default_factory=CostCalculator)\n    client: Optional[AsyncOpenAI] = Field(default=None, exclude=True)\n    spent: float = Field(default=0.0, description=\"Total accumulated cost for this instance\")\n    \n    model_config = ConfigDict(arbitrary_types_allowed=True)\n\n    def __init__(self, profile_or_config: Union[str, Dict[str, Any]] = \"default\", **kwargs):\n        if isinstance(profile_or_config, str):\n            config = self._load_profile_config(profile_or_config)\n            config.update({k: v for k, v in kwargs.items() if k in LLMConfig.model_fields})\n            super().__init__(config=LLMConfig(**config))\n        else:\n            config_kwargs = profile_or_config if isinstance(profile_or_config, dict) else {}\n            config_kwargs.update({k: v for k, v in kwargs.items() if k in LLMConfig.model_fields})\n            super().__init__(config=LLMConfig(**config_kwargs))\n            \n        self._initialize_client()\n    \n    def _load_profile_config(self, profile: str) -> Dict[str, Any]:\n        try:\n            config_path = DEFAULT_LLM_PROFILE_PATH\n            with open(config_path, 'r', encoding='utf-8') as f:\n                config = yaml.safe_load(f)\n                \n            if profile in config.get(\"models\", {}):\n                return config[\"models\"][profile]\n            elif profile in config.get(\"llm_pool\", {}):\n                pool_config = config[\"llm_pool\"][profile]\n                return {k: v for k, v in pool_config.items() if k in LLMConfig.model_fields}\n            else:\n                return {}\n                \n        except Exception as e:\n            return {}\n        \n    def _initialize_client(self) -> None:\n        if not self.config.api_key:\n            self.config.api_key = os.environ.get(\"OPENAI_API_KEY\")\n            if not self.config.api_key:\n                raise ValueError(\"Missing required API key. Set OPENAI_API_KEY environment variable.\")\n        \n        client_args = {\n            \"api_key\": self.config.api_key,\n            \"timeout\": self.config.timeout\n        }\n        \n        if self.config.base_url:\n            client_args[\"base_url\"] = self.config.base_url\n            \n        self.client = AsyncOpenAI(**client_args)\n\n    async def __call__(\n        self,\n        prompt: str,\n        system_prompt: Optional[str] = None,\n        **generation_args\n    ) -> Tuple[str, float]:\n        messages = self._build_messages(prompt, system_prompt)\n        params = self._prepare_params(messages, generation_args)\n\n        response = await self._retry_api_call(params)\n        content = response.choices[0].message.content\n        \n        cost = 0.0\n        if self.config.track_costs and (usage := getattr(response, \"usage\", None)):\n            cost, _ = self.cost_calculator.compute_cost(\n                response.model,\n                usage.prompt_tokens,\n                usage.completion_tokens\n            )\n        self.spent += cost\n        \n        return content, cost\n\n    def _build_messages(self, prompt: str, system_prompt: Optional[str]) -> List[Dict[str, str]]:\n        messages = []\n        if system_prompt:\n            messages.append({\"role\": \"system\", \"content\": system_prompt})\n        messages.append({\"role\": \"user\", \"content\": prompt})\n        return messages\n\n    def _prepare_params(\n        self,\n        messages: list[Dict[str, str]],\n        generation_args: Dict[str, Any]\n    ) -> Dict[str, Any]:\n        params: Dict[str, Any] = {\n            \"model\": self.config.model,\n            \"messages\": messages,\n        }\n\n        model_name = (self.config.model or \"\").lower()\n        is_o_series = model_name.startswith(\"o\")\n\n        if (self.config.temperature is not None) and (not is_o_series):\n            params[\"temperature\"] = self.config.temperature\n\n        if self.config.max_tokens is not None:\n            params[\"max_tokens\"] = self.config.max_tokens\n\n        safe_generation_args = dict(generation_args) if generation_args else {}\n        if is_o_series:\n            safe_generation_args.pop(\"temperature\", None)\n\n        params.update(safe_generation_args)\n        return params\n\n    async def _retry_api_call(self, params: Dict[str, Any]) -> Any:\n        for attempt in range(self.config.max_retries + 1):\n            try:\n                return await self.client.chat.completions.create(**params)\n            except (APIError, APIConnectionError, APITimeoutError, RateLimitError) as e:\n                if attempt == self.config.max_retries:\n                    raise\n                backoff_time = self._calculate_backoff(\n                    attempt,\n                    self.config.retry_base_delay,\n                    self.config.timeout\n                )\n                await asyncio.sleep(backoff_time)\n\n    def _calculate_backoff(self, attempt: int, base: float, max_wait: float) -> float:\n        delay = base * (2 ** attempt)\n        jitter = delay * self.config.retry_jitter * random.uniform(-1, 1)\n        return min(delay + jitter, max_wait)\n\ndef create_llm_instance(model_name: str) -> AsyncLLM:\n    return AsyncLLM(profile_or_config=model_name)\n\nasync def main():\n    try:\n        llm = AsyncLLM(\"default\")\n        prompt = \"Hello, what is the capital of France?\"\n        response, cost = await llm(prompt)\n        print(\"Response:\", response)\n        print(\"Cost:\", cost)\n    except Exception as e:\n        print(f\"An error occurred: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())"
  },
  {
    "path": "utils/logger.py",
    "content": "import os\nimport logging\nfrom datetime import datetime\nfrom pathlib import Path\n\nclass MultiLineFormatter(logging.Formatter):\n    def format(self, record):\n        msg = super().format(record)\n        \n        lines = msg.split('\\n')\n        if len(lines) <= 1:\n            return msg\n            \n        return '\\n'.join(lines)\n\nclass SimpleLogger:\n    def __init__(self, run_id=None, log_level=logging.INFO):\n        if run_id is None:\n            run_id = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n        \n        self.run_id = run_id\n        self.base_dir = Path(\"logs\") / run_id\n        self.log_dir = self.base_dir / \"running_logs\"\n        \n        self.log_dir.mkdir(parents=True, exist_ok=True)\n        \n        sanitized_run_id = run_id.replace(\"/\", \"_\").replace(\"\\\\\", \"_\")\n        self.logger = logging.getLogger(f\"alfworld_run_{sanitized_run_id}\")\n        self.logger.setLevel(log_level)\n        \n        self.logger.handlers.clear()\n        \n        log_file = self.log_dir / \"run.log\"\n        file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')\n        file_handler.setLevel(log_level)\n        \n        console_handler = logging.StreamHandler()\n        console_handler.setLevel(log_level)\n        \n        formatter = MultiLineFormatter('%(asctime)s - %(levelname)s - %(message)s')\n        file_handler.setFormatter(formatter)\n        console_handler.setFormatter(formatter)\n        \n        self.logger.addHandler(file_handler)\n        self.logger.addHandler(console_handler)\n        \n        self.info(f\"Starting new run with ID: {run_id}\")\n        self.info(f\"Logs will be saved to: {self.log_dir.absolute()}\")\n    \n    def info(self, message):\n        self.logger.info(message)\n    \n    def error(self, message):\n        self.logger.error(message)\n    \n    def warning(self, message):\n        self.logger.warning(message)\n    \n    def debug(self, message):\n        self.logger.debug(message)\n    \n    def log_result(self, result):\n        task_id = result.get(\"task_id\", \"unknown\")\n        success = result.get(\"both_success\", result.get(\"is_success\", False))\n        exec_time = result.get(\"execution_time\", result.get(\"time\", 0))\n        game_name = result.get(\"game_name\", \"\")\n        \n        status = \"SUCCESS\" if success else \"FAILED\"\n        self.info(f\"[{status}] {task_id} - {game_name} - {exec_time:.2f}s\")\n        \n        if \"error\" in result:\n            self.error(f\"Error in {task_id}: {result['error']}\")\n    \n    def log_stats(self, stats):\n        self.info(\"=\" * 50)\n        self.info(\"RUN STATISTICS\")\n        self.info(\"=\" * 50)\n        self.info(f\"Total tests: {stats['total_tests']}\")\n        self.info(f\"Successful: {stats['successful_tests']}\")\n        self.info(f\"Success rate: {stats['success_rate']:.1%}\")\n        self.info(f\"Average execution time: {stats['average_execution_time']:.2f}s\")\n        \n        if stats.get('task_types'):\n            self.info(\"\\nSuccess rate by task type:\")\n            for task_type, type_stats in stats['task_types'].items():\n                rate = type_stats['rate']\n                total = type_stats['total']\n                success = type_stats['success']\n                self.info(f\"  {task_type}: {success}/{total} ({rate:.1%})\")\n    \n    def get_log_dir(self):\n        return self.log_dir\n\n    def get_base_dir(self):\n        return self.base_dir "
  },
  {
    "path": "utils/mockllm.py",
    "content": "import asyncio\n\nclass MockLLM:\n    \n    def __init__(self, name=\"MockLLM\"):\n        self.name = name\n        \n    async def __call__(self, prompt):\n        print(f\"\\n--- {self.name} Prompt ---\")\n        print(prompt)\n        print(f\"\\n--- Please provide your response (enter an empty line to finish) ---\")\n        \n        lines = []\n        while True:\n            line = input()\n            if line.strip() == \"\":\n                break\n            lines.append(line)\n        \n        return \"\\n\".join(lines)\n\n\nasync def test_mock_llm():\n    mock_llm = MockLLM(name=\"TestLLM\")\n    \n    prompts = [\n        \"What is the capital of France?\",\n        \"Write a short poem about artificial intelligence.\"\n    ]\n    \n    for i, prompt in enumerate(prompts, 1):\n        print(f\"\\nTest {i}:\")\n        response = await mock_llm(prompt)\n        print(\"\\nYour response was:\")\n        print(\"-\" * 40)\n        print(response)\n        print(\"-\" * 40)\n    \n    print(\"\\nTest completed successfully!\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(test_mock_llm())\n"
  }
]