| """In-episode hypothesis and issue tracker for the DataForge RL agent. |
| |
| The scratchpad is a mutable, episode-scoped data structure that the agent |
| uses to record hypotheses, confirmed issues, and dead ends. The environment |
| exposes a compact summary of the scratchpad in each observation, enabling |
| the agent to reason about its investigation history without direct access |
| to the underlying data structure. |
| |
| Example:: |
| |
| >>> from dataforge.agent.scratchpad import Scratchpad |
| >>> pad = Scratchpad() |
| >>> pad.add_hypothesis("Rating column has decimal shift", [5], ["rating"], "decimal_shift") |
| >>> pad.confirm_issue(5, "rating", "decimal_shift") |
| >>> pad.summary() |
| 'Hypotheses: 1 (0 pending). Confirmed: 1. Dead ends: 0.' |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
|
|
| __all__ = [ |
| "ConfirmedIssue", |
| "DeadEnd", |
| "HypothesisRecord", |
| "Scratchpad", |
| ] |
|
|
|
|
| @dataclass(frozen=True) |
| class HypothesisRecord: |
| """A recorded hypothesis about a data-quality root cause. |
| |
| Args: |
| claim: Textual description of the hypothesis. |
| affected_rows: Row indices the hypothesis covers. |
| affected_columns: Column names the hypothesis covers. |
| root_cause_type: Detector-vocabulary root cause type. |
| confirmed: Whether the hypothesis was confirmed by ground truth. |
| """ |
|
|
| claim: str |
| affected_rows: tuple[int, ...] |
| affected_columns: tuple[str, ...] |
| root_cause_type: str |
| confirmed: bool = False |
|
|
|
|
| @dataclass(frozen=True) |
| class ConfirmedIssue: |
| """A confirmed data-quality issue at a specific location. |
| |
| Args: |
| row: Zero-indexed row number. |
| column: Column name. |
| issue_type: Issue type classification. |
| """ |
|
|
| row: int |
| column: str |
| issue_type: str |
|
|
|
|
| @dataclass(frozen=True) |
| class DeadEnd: |
| """A recorded dead end — an investigation path that yielded nothing. |
| |
| Args: |
| description: What was tried and why it failed. |
| step_number: Step at which the dead end was recorded. |
| """ |
|
|
| description: str |
| step_number: int |
|
|
|
|
| @dataclass |
| class Scratchpad: |
| """Mutable in-episode tracker for hypotheses, confirmed issues, and dead ends. |
| |
| Reset at the start of each episode. The ``summary()`` method produces a |
| compact string for inclusion in agent observations. |
| |
| Example:: |
| |
| >>> pad = Scratchpad() |
| >>> pad.add_hypothesis("Decimal shift in rating", [5], ["rating"], "decimal_shift") |
| >>> len(pad.hypotheses) |
| 1 |
| """ |
|
|
| hypotheses: list[HypothesisRecord] = field(default_factory=list) |
| confirmed_issues: list[ConfirmedIssue] = field(default_factory=list) |
| dead_ends: list[DeadEnd] = field(default_factory=list) |
|
|
| def add_hypothesis( |
| self, |
| claim: str, |
| affected_rows: list[int], |
| affected_columns: list[str], |
| root_cause_type: str, |
| ) -> HypothesisRecord: |
| """Record a new hypothesis. |
| |
| Args: |
| claim: Textual description of the hypothesis. |
| affected_rows: Row indices the hypothesis covers. |
| affected_columns: Column names the hypothesis covers. |
| root_cause_type: Detector-vocabulary root cause type. |
| |
| Returns: |
| The recorded hypothesis. |
| """ |
| record = HypothesisRecord( |
| claim=claim, |
| affected_rows=tuple(affected_rows), |
| affected_columns=tuple(affected_columns), |
| root_cause_type=root_cause_type, |
| ) |
| self.hypotheses.append(record) |
| return record |
|
|
| def confirm_hypothesis(self, index: int) -> None: |
| """Mark a hypothesis as confirmed. |
| |
| Args: |
| index: Index into the ``hypotheses`` list. |
| |
| Raises: |
| IndexError: If the index is out of range. |
| """ |
| old = self.hypotheses[index] |
| self.hypotheses[index] = HypothesisRecord( |
| claim=old.claim, |
| affected_rows=old.affected_rows, |
| affected_columns=old.affected_columns, |
| root_cause_type=old.root_cause_type, |
| confirmed=True, |
| ) |
|
|
| def confirm_issue(self, row: int, column: str, issue_type: str) -> None: |
| """Record a confirmed issue. |
| |
| Args: |
| row: Zero-indexed row number. |
| column: Column name. |
| issue_type: Issue type classification. |
| """ |
| self.confirmed_issues.append(ConfirmedIssue(row=row, column=column, issue_type=issue_type)) |
|
|
| def add_dead_end(self, description: str, step_number: int) -> None: |
| """Record a dead end. |
| |
| Args: |
| description: What was tried and why it failed. |
| step_number: Step at which the dead end was recorded. |
| """ |
| self.dead_ends.append(DeadEnd(description=description, step_number=step_number)) |
|
|
| def reset(self) -> None: |
| """Clear all tracked state for a new episode.""" |
| self.hypotheses.clear() |
| self.confirmed_issues.clear() |
| self.dead_ends.clear() |
|
|
| def summary(self) -> str: |
| """Produce a compact summary string for observation embedding. |
| |
| Returns: |
| A one-line summary of scratchpad state. |
| |
| Example:: |
| |
| >>> Scratchpad().summary() |
| 'Hypotheses: 0 (0 pending). Confirmed: 0. Dead ends: 0.' |
| """ |
| pending = sum(1 for h in self.hypotheses if not h.confirmed) |
| return ( |
| f"Hypotheses: {len(self.hypotheses)} ({pending} pending). " |
| f"Confirmed: {len(self.confirmed_issues)}. " |
| f"Dead ends: {len(self.dead_ends)}." |
| ) |
|
|