Modern QA2026Implementing the Agent in Python — tiles
Log inJoin
13 / 108 · 03 Agentic Testing Architectures · Mapping ReAct to CLI Browser Commands← prev⊞ allnext →☰ Read as one page

2.4Implementing the Agent in Python

import subprocess
import json
from pathlib import Path

class CliReActAgent:
    """A ReAct test agent driving the browser via playwright-cli."""

    def __init__(self, llm, max_steps=15):
        self.llm = llm
        self.max_steps = max_steps
        self.history = []

    def cli(self, command: str) -> str:
        """Execute a playwright-cli command and return its (minimal) output."""
        result = subprocess.run(
            f"playwright-cli {command}",
            shell=True, capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return f"ERROR: {result.stderr.strip()}"
        return result.stdout.strip()

    def observe(self) -> dict:
        """Gather current page state. Snapshot goes to disk; read it back."""
        snapshot_path = self.cli("snapshot")          # returns a file path
        snapshot = ""
        if snapshot_path and not snapshot_path.startswith("ERROR"):
            snapshot = Path(snapshot_path.split()[-1]).read_text()[:2000]
        return {
            "url": self.cli('eval "location.href"'),
            "snapshot": snapshot,
            "errors": self.cli('eval "document.querySelectorAll(\'.error\').length"'),
        }

    def run(self, objective: str) -> dict:
        """Execute a test using the ReAct loop."""
        for step in range(self.max_steps):
            # OBSERVE
            state = self.observe()

            # THINK + decide
            prompt = f"""
            You are a QA test agent. Your objective: {objective}

            Current state:
            - URL: {state['url']}
            - Page snapshot (YAML, first 2000 chars): {state['snapshot']}
            - Error count: {state['errors']}

            History (last 5 actions):
            {json.dumps(self.history[-5:], indent=2)}

            Respond with exactly ONE action:
            CLI <command>   — execute a playwright-cli command (use refs from the snapshot)
            PASS <reason>   — test passed, explain why
            FAIL <reason>   — test failed, explain why
            """

            response = self.llm.generate(prompt).strip()
            self.history.append({"step": step, "state_url": state["url"], "action": response})

            # ACT or EVALUATE
            if response.startswith("CLI "):
                command = response[4:]
                output = self.cli(command)
                self.history[-1]["output"] = output
            elif response.startswith("PASS "):
                return {"status": "pass", "reason": response[5:], "steps": step + 1}
            elif response.startswith("FAIL "):
                return {"status": "fail", "reason": response[5:], "steps": step + 1}

        return {"status": "timeout", "reason": f"Exceeded {self.max_steps} steps", "steps": self.max_steps}

Note what the disk-first design does to this loop: the full snapshot only enters the LLM prompt when observe() deliberately includes it. A production version would be smarter still — passing the snapshot path and letting the agent read it only when its reasoning requires.