from __future__ import annotations import argparse import json import sys from pathlib import Path from .config import load_app_config from .models import RuntimeOptions from .runtime import create_openai_client, run_job from .safety import assess_task_safety def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Run an autonomous desktop task agent using OpenAI + local tools.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" ' python main.py run "Open amazon.de"\n' ' python main.py run "Open amazon.de and search for keyboard" --max-steps 80\n' ' python main.py run "Open amazon.de" --disable-tool click --disable-tool type\n' ), ) parser.add_argument("job", type=str, help="Task objective for the agent.") parser.add_argument("--model", type=str, default=None, help="OpenAI model name.") parser.add_argument("--max-steps", type=int, default=60, help="Max tool-iteration steps.") parser.add_argument("--command-timeout", type=int, default=45, help="Timeout in seconds for execute_command.") parser.add_argument("--type-interval", type=float, default=0.02, help="Seconds between typed characters.") parser.add_argument("--click-pause", type=float, default=0.10, help="Mouse move duration before click.") parser.add_argument( "--reasoning-effort", choices=["low", "medium", "high"], default="medium", help="Reasoning effort passed to the model.", ) parser.add_argument( "--screen-context-decay-steps", type=int, default=4, help="Compact model context every N steps to decay old screenshots (0 disables).", ) parser.add_argument("--disable-tool", action="append", default=[], help="Disable a tool by name.") parser.add_argument("--skip-safety-check", action="store_true", help="Bypass pre-flight safety check.") parser.add_argument("--no-failsafe", action="store_true", help="Disable PyAutoGUI fail-safe.") return parser def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) cwd = Path.cwd() config = load_app_config(cwd) if not config.openai_api_key: print("ERROR: Missing OPENAI_API_KEY in environment/.env", file=sys.stderr) return 2 model = args.model or config.default_model disabled_tools = sorted({str(x).strip() for x in args.disable_tool if str(x).strip()}) if not args.skip_safety_check: safety_client = create_openai_client(config.openai_api_key) safe, reason, parsed = assess_task_safety( safety_client, model=config.safety_model, objective=args.job, disabled_tools=disabled_tools, ) if not safe: print( json.dumps( { "completed": False, "result": f"Blocked by safety check: {reason}", "response": {"return": f"Blocked by safety check: {reason}", "data": parsed}, "return": f"Blocked by safety check: {reason}", "data": parsed, "safety": parsed, }, ensure_ascii=False, indent=2, ) ) return 1 options = RuntimeOptions( model=model, max_steps=args.max_steps, command_timeout=args.command_timeout, type_interval=args.type_interval, click_pause=args.click_pause, reasoning_effort=args.reasoning_effort, screen_context_decay_steps=max(0, int(args.screen_context_decay_steps)), disable_tools=set(disabled_tools), ) try: result, artifacts = run_job( api_key=config.openai_api_key, objective=args.job, options=options, runs_base=config.runs_dir, no_failsafe=args.no_failsafe, ) except KeyboardInterrupt: print(json.dumps({"completed": False, "result": "Interrupted by user."}, ensure_ascii=False, indent=2)) return 130 except Exception as exc: # noqa: BLE001 print( json.dumps( {"completed": False, "result": f"Fatal error: {type(exc).__name__}: {exc}"}, ensure_ascii=False, indent=2, ), file=sys.stderr, ) return 1 payload = { "completed": result.completed, "result": result.return_message, "response": {"return": result.return_message, "data": result.data}, "return": result.return_message, "data": result.data, "steps": result.steps, "elapsed_seconds": round(result.ended_at - result.started_at, 3), "artifacts_dir": str(artifacts.root_dir.resolve()), "usage": result.usage.to_dict(), "error": result.error, "cancelled": result.cancelled, } print(json.dumps(payload, ensure_ascii=False, indent=2)) return 0 if result.completed else 1