| """Instruction parser: extract ordered subgoal sequences from LIBERO task instructions.""" |
|
|
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass, field |
| from typing import List, Optional |
|
|
|
|
| @dataclass |
| class Subgoal: |
| type: str |
| object: str |
| target: str = "" |
| descriptor: str = "" |
| relation: str = "" |
|
|
| def label(self) -> str: |
| """Label for phase matching. Does NOT include descriptor/relation.""" |
| parts = [self.type, self.object] |
| if self.target: |
| parts.append(self.target) |
| return "_".join(parts).replace(" ", "_") |
|
|
|
|
| |
| VERB_PATTERNS = [ |
| ("pick up", "pick_up"), |
| ("turn on", "turn_on"), |
| ("turn off", "turn_off"), |
| ("put", "put"), |
| ("place", "place"), |
| ("open", "open"), |
| ("close", "close"), |
| ("push", "push"), |
| ("stack", "stack"), |
| ] |
|
|
| |
| |
| PREP_PATTERN = re.compile( |
| r"\b(?:on top of|to the (?:right|left|front|back) of|next to|between|from|on|in|to|inside)\b" |
| ) |
|
|
|
|
| def _find_verb_positions(instruction: str) -> List[tuple]: |
| """Find all verb occurrences and their positions in the instruction.""" |
| found = [] |
| lower = instruction.lower() |
| for verb_text, verb_id in VERB_PATTERNS: |
| start = 0 |
| while True: |
| idx = lower.find(verb_text, start) |
| if idx == -1: |
| break |
| |
| if idx > 0 and lower[idx - 1].isalpha(): |
| start = idx + 1 |
| continue |
| found.append((idx, verb_text, verb_id)) |
| start = idx + len(verb_text) |
| |
| found.sort(key=lambda x: x[0]) |
| return found |
|
|
|
|
| def _split_object_target(clause: str) -> tuple: |
| """Split a clause into (object_str, prep_str, target_str) using preposition.""" |
| m = PREP_PATTERN.search(clause) |
| if m: |
| obj = clause[: m.start()].strip() |
| prep = m.group() |
| tgt = clause[m.end() :].strip() |
| return obj, prep, tgt |
| return clause.strip(), "", "" |
|
|
|
|
| def _clean_noun(text: str) -> str: |
| """Clean up a noun phrase: strip articles and extra whitespace.""" |
| text = text.strip() |
| |
| text = re.sub(r"^the\s+", "", text) |
| |
| text = re.sub(r"^both\s+", "", text) |
| |
| text = re.sub(r"\s+", " ", text) |
| return text.strip() |
|
|
|
|
| def _clean_all_articles(text: str) -> str: |
| """Strip all occurrences of 'the' articles (not just leading).""" |
| text = re.sub(r"\bthe\s+", "", text) |
| return re.sub(r"\s+", " ", text).strip() |
|
|
|
|
| def parse_instruction(instruction: str) -> List[Subgoal]: |
| """Parse a LIBERO instruction string into an ordered list of Subgoals.""" |
| lower = instruction.lower().strip() |
| verb_positions = _find_verb_positions(lower) |
|
|
| if not verb_positions: |
| raise ValueError(f"No verbs found in instruction: {instruction}") |
|
|
| |
| clauses = [] |
| for i, (pos, verb_text, verb_id) in enumerate(verb_positions): |
| clause_start = pos + len(verb_text) |
| if i + 1 < len(verb_positions): |
| |
| next_pos = verb_positions[i + 1][0] |
| clause_end = next_pos |
| |
| raw = lower[clause_start:clause_end] |
| raw = re.sub(r"\s+and\s*$", "", raw) |
| else: |
| raw = lower[clause_start:] |
| clauses.append((verb_id, raw.strip())) |
|
|
| |
| subgoals: List[Subgoal] = [] |
| context = {"last_object": "", "last_target": ""} |
|
|
| for verb_id, raw_clause in clauses: |
| new_subgoals = _process_clause(verb_id, raw_clause, context) |
| subgoals.extend(new_subgoals) |
| |
| if new_subgoals: |
| last = new_subgoals[-1] |
| if last.object: |
| context["last_object"] = last.object |
| if last.target: |
| context["last_target"] = last.target |
|
|
| return subgoals |
|
|
|
|
| def _resolve_pronoun(text: str, context: dict, role: str) -> str: |
| """Resolve pronouns ('it') and implicit references from context. |
| |
| Fallback logic: if the preferred context field is empty, try the other one. |
| e.g. 'turn on the stove and put X on it' → 'it' as target should resolve |
| to 'stove' (last_object) since turn_on has no target. |
| """ |
| cleaned = _clean_noun(text) |
| if cleaned in ("it", "them"): |
| if role == "object": |
| return context.get("last_object", "") or context.get("last_target", cleaned) |
| else: |
| return context.get("last_target", "") or context.get("last_object", cleaned) |
| if not cleaned: |
| if role == "object": |
| return context.get("last_object", "") or context.get("last_target", "") |
| else: |
| return context.get("last_target", "") or context.get("last_object", "") |
| return cleaned |
|
|
|
|
| def _process_clause( |
| verb_id: str, raw_clause: str, context: dict |
| ) -> List[Subgoal]: |
| """Process a single verb clause into subgoal(s).""" |
|
|
| |
| if verb_id == "put" and "both" in raw_clause: |
| return _handle_put_both(raw_clause, context) |
|
|
| |
| if verb_id == "put": |
| obj_str, prep, tgt_str = _split_object_target(raw_clause) |
| obj = _clean_noun(obj_str) |
| tgt = _resolve_pronoun(tgt_str, context, "target") if tgt_str else "" |
| relation = prep |
| |
| if not tgt and "inside" in raw_clause: |
| tgt = context.get("last_target", "") or context.get("last_object", "") |
| relation = "inside" |
| return [ |
| Subgoal(type="pick", object=obj, target=""), |
| Subgoal(type="place", object=obj, target=tgt, relation=relation), |
| ] |
|
|
| |
| if verb_id == "pick_up": |
| obj_str, prep, tgt_str = _split_object_target(raw_clause) |
| obj = _clean_noun(obj_str) |
| descriptor = "" |
| if prep and tgt_str: |
| descriptor = f"{prep} {_clean_all_articles(tgt_str)}" |
| return [Subgoal(type="pick", object=obj, descriptor=descriptor)] |
|
|
| |
| if verb_id == "place": |
| obj_str, prep, tgt_str = _split_object_target(raw_clause) |
| obj = _resolve_pronoun(obj_str, context, "object") |
| tgt = _clean_noun(tgt_str) if tgt_str else "" |
| relation = prep |
| return [Subgoal(type="place", object=obj, target=tgt, relation=relation)] |
|
|
| |
| if verb_id == "open": |
| obj = _clean_noun(raw_clause) |
| return [Subgoal(type="open", object=obj)] |
|
|
| |
| if verb_id == "close": |
| obj = _resolve_pronoun(raw_clause, context, "target") |
| return [Subgoal(type="close", object=obj)] |
|
|
| |
| if verb_id == "turn_on": |
| obj = _clean_noun(raw_clause) |
| return [Subgoal(type="turn_on", object=obj)] |
|
|
| |
| if verb_id == "turn_off": |
| obj = _clean_noun(raw_clause) |
| return [Subgoal(type="turn_off", object=obj)] |
|
|
| |
| if verb_id == "push": |
| obj_str, prep, tgt_str = _split_object_target(raw_clause) |
| obj = _clean_noun(obj_str) |
| tgt = _clean_noun(tgt_str) if tgt_str else "" |
| relation = prep |
| return [Subgoal(type="push", object=obj, target=tgt, relation=relation)] |
|
|
| |
| if verb_id == "stack": |
| obj_str, prep, tgt_str = _split_object_target(raw_clause) |
| obj = _clean_noun(obj_str) |
| tgt = _clean_noun(tgt_str) if tgt_str else "" |
| relation = prep |
| return [Subgoal(type="place", object=obj, target=tgt, relation=relation)] |
|
|
| raise ValueError(f"Unknown verb_id: {verb_id}") |
|
|
|
|
| def _handle_put_both(raw_clause: str, context: dict) -> List[Subgoal]: |
| """Handle 'put both X and Y in/on Z' pattern.""" |
| |
| clause = raw_clause.replace("both", "").strip() |
|
|
| |
| obj_str, prep, tgt_str = _split_object_target(clause) |
| tgt = _clean_noun(tgt_str) |
| relation = prep |
|
|
| |
| |
| parts = re.split(r"\s+and\s+", obj_str) |
| if len(parts) == 2: |
| obj1 = _clean_noun(parts[0]) |
| obj2 = _clean_noun(parts[1]) |
| elif len(parts) == 1: |
| |
| |
| name = _clean_noun(parts[0]) |
| |
| if name.endswith("s") and not name.endswith("ss"): |
| name = name[:-1] |
| obj1 = name |
| obj2 = name |
| else: |
| |
| obj1 = _clean_noun(parts[0]) |
| obj2 = _clean_noun(parts[-1]) |
|
|
| subgoals = [ |
| Subgoal(type="pick", object=obj1), |
| Subgoal(type="place", object=obj1, target=tgt, relation=relation), |
| Subgoal(type="pick", object=obj2), |
| Subgoal(type="place", object=obj2, target=tgt, relation=relation), |
| ] |
| return subgoals |
|
|
|
|
| if __name__ == "__main__": |
| |
| test_instructions = [ |
| "pick up the orange juice and place it in the basket", |
| "put the bowl on the plate", |
| "open the top drawer and put the bowl inside", |
| "put both the alphabet soup and the cream cheese box in the basket", |
| "turn on the stove and put the moka pot on it", |
| "put the black bowl in the bottom drawer of the cabinet and close it", |
| ] |
| for inst in test_instructions: |
| subgoals = parse_instruction(inst) |
| labels = [sg.label() for sg in subgoals] |
| print(f"{inst!r}") |
| print(f" -> {labels}") |
| print() |
|
|