Files
screenjob/src/cli.py
Space-Banane 4123765aba
Some checks failed
CI / test (push) Failing after 8s
Commit remaining workspace updates
2026-05-31 20:43:36 +02:00

193 lines
7.1 KiB
Python

from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from .agent import normalize_disabled_tools
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(
"--max-visual-context-images",
type=int,
default=3,
help="Maximum screenshots/enhanced images retained in model-visible context during rebases.",
)
parser.add_argument(
"--native-automation-mode",
choices=["off", "prefer", "require_fallback"],
default="prefer",
help="How strongly the agent should prefer Windows-native automation helpers over pixel fallback.",
)
parser.add_argument(
"--dialog-timeout-seconds",
type=float,
default=12.0,
help="Timeout for dialog-oriented waits and retries.",
)
parser.add_argument(
"--focus-timeout-seconds",
type=float,
default=8.0,
help="Timeout for focus-change waits and verification.",
)
parser.add_argument(
"--ui-element-timeout-seconds",
type=float,
default=8.0,
help="Timeout for native UI element lookup waits.",
)
parser.add_argument(
"--max-retries-per-surface",
type=int,
default=3,
help="Maximum repeated retries on the same classified window/dialog surface before the agent must pivot.",
)
parser.add_argument(
"--pretty-logs",
action="store_true",
help="Emit expanded multi-line tool call/result logs for easier debugging.",
)
parser.add_argument("--disable-tool", action="append", default=[], help="Disable a tool by name.")
parser.add_argument(
"--skip-safety-check",
"--skip-safety-chec",
dest="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
try:
disabled_tools = normalize_disabled_tools(args.disable_tool)
except ValueError as exc:
parser.error(str(exc))
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)),
max_visual_context_images=max(0, int(args.max_visual_context_images)),
native_automation_mode=args.native_automation_mode,
dialog_timeout_seconds=max(0.5, float(args.dialog_timeout_seconds)),
focus_timeout_seconds=max(0.5, float(args.focus_timeout_seconds)),
ui_element_timeout_seconds=max(0.5, float(args.ui_element_timeout_seconds)),
max_retries_per_surface=max(1, int(args.max_retries_per_surface)),
pretty_logs=bool(args.pretty_logs),
disable_tools=set(disabled_tools),
prohibited_key_combos=set(config.prohibited_key_combos),
)
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