"""Raw ReAct loop in plain Python, no agents framework. Companion to the All Things Agentic post 'Why Your Agent Goes In Circles'. The agent investigates failing pytests in sample_repo/, decides each next step from what it just saw, and stops when it has a final answer (or hits MAX_ITERS). Run: uv sync export OPENAI_API_KEY=sk-... uv run python raw_react.py """ from __future__ import annotations import json import os import subprocess import sys from pathlib import Path from dotenv import load_dotenv from openai import OpenAI load_dotenv() # searches upward for a .env file; picks up the repo root's MODEL = "gpt-5" MAX_ITERS = 10 SAMPLE_REPO = (Path(__file__).parent / "sample_repo").resolve() TOOLS = [ { "type": "function", "function": { "name": "run_pytest", "description": "Run pytest against sample_repo/ and return stdout.", "parameters": {"type": "object", "properties": {}, "required": []}, }, }, { "type": "function", "function": { "name": "read_file", "description": "Read a file inside sample_repo/. Refuses paths outside it.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path relative to sample_repo/", }, }, "required": ["path"], }, }, }, ] def _run_pytest() -> str: result = subprocess.run( [sys.executable, "-m", "pytest", str(SAMPLE_REPO), "--tb=short", "-q"], capture_output=True, text=True, timeout=60, ) return (result.stdout + result.stderr).strip() def _read_file(path: str) -> str: target = (SAMPLE_REPO / path).resolve() if SAMPLE_REPO not in target.parents and target != SAMPLE_REPO: return f"refused: path '{path}' is outside sample_repo/" if not target.exists(): return f"not found: {path}" return target.read_text() def dispatch(name: str, args: dict) -> str: if name == "run_pytest": return _run_pytest() if name == "read_file": return _read_file(args["path"]) return f"unknown tool: {name}" SYSTEM = """You investigate failing pytests in sample_repo/. Use run_pytest to see the failures, read_file to inspect each failing test and the implementation it imports, then cluster the failures by root cause and give a final short summary. When you have a final summary, reply with text and no tool calls. """ def main() -> int: if not os.environ.get("OPENAI_API_KEY"): print("OPENAI_API_KEY is not set", file=sys.stderr) return 1 client = OpenAI() messages = [ {"role": "system", "content": SYSTEM}, {"role": "user", "content": "Triage the failing tests in sample_repo/."}, ] last_action = None for i in range(MAX_ITERS): resp = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, ) msg = resp.choices[0].message if not msg.tool_calls: print(f"iter {i}: final answer\n") print(msg.content) return 0 messages.append(msg) for call in msg.tool_calls: args = json.loads(call.function.arguments or "{}") action = f"{call.function.name}({args})" print(f"iter {i}: calling {action}") if action == last_action: print(f"iter {i}: stuck (same call as last iter); breaking") return 0 last_action = action messages.append( { "role": "tool", "tool_call_id": call.id, "content": dispatch(call.function.name, args), } ) print("(stopped: hit MAX_ITERS without a final answer)") return 0 if __name__ == "__main__": raise SystemExit(main())