openra-rl / examples /llm_agent.py
github-actions[bot]
Sync from GitHub ac82c3e
02f4a63
#!/usr/bin/env python3
"""LLM agent that plays Red Alert using any OpenAI-compatible model.
Supports OpenRouter, Ollama, LM Studio, or any local/remote endpoint
that implements the OpenAI Chat Completions API with tool calling.
Usage:
# With OpenRouter (cloud)
export OPENROUTER_API_KEY=sk-or-...
python examples/llm_agent.py --verbose
# With a YAML config file
python examples/llm_agent.py --config examples/config-ollama.yaml
# With LM Studio (local, no API key needed)
python examples/llm_agent.py --base-url http://localhost:1234/v1/chat/completions --model my-model
"""
import argparse
import asyncio
import sys
from dotenv import load_dotenv
load_dotenv()
from openra_env.config import load_config
from openra_env.agent import run_agent
# Re-export for backwards compatibility
from openra_env.agent import ( # noqa: F401
SYSTEM_PROMPT,
load_system_prompt,
compose_pregame_briefing,
format_state_briefing,
mcp_tools_to_openai,
_sanitize_messages,
chat_completion,
compress_history,
)
# Line-buffered stdout so output is observable in real time
sys.stdout.reconfigure(line_buffering=True)
def main():
parser = argparse.ArgumentParser(
description="LLM agent that plays Red Alert via any OpenAI-compatible model",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" %(prog)s --config examples/config-ollama.yaml --verbose\n"
" %(prog)s --api-key sk-or-... --verbose\n"
" %(prog)s --base-url http://localhost:1234/v1/chat/completions --model my-model\n"
),
)
parser.add_argument(
"--config", "-c",
default=None,
help="Path to YAML config file (default: auto-discover config.yaml)",
)
parser.add_argument(
"--url",
default=None,
help="OpenRA-RL server URL (overrides config agent.server_url)",
)
parser.add_argument(
"--base-url",
default=None,
help="LLM API endpoint URL (overrides config llm.base_url)",
)
parser.add_argument(
"--model",
default=None,
help="Model ID (overrides config llm.model)",
)
parser.add_argument(
"--api-key",
default=None,
help="API key for LLM endpoint (overrides config llm.api_key)",
)
parser.add_argument(
"--max-turns",
type=int,
default=None,
help="Maximum LLM turns, 0 = unlimited (overrides config agent.max_turns)",
)
parser.add_argument(
"--max-time",
type=int,
default=None,
help="Maximum wall-clock time in seconds (overrides config agent.max_time_s)",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Print detailed LLM reasoning and tool calls",
)
parser.add_argument(
"--log-file",
default=None,
help="Write all output to this log file in addition to stdout",
)
parser.add_argument(
"--system-prompt",
default=None,
help="Path to a custom system prompt .txt file (overrides built-in default)",
)
args = parser.parse_args()
# Build config: YAML file + env vars + CLI overrides (CLI wins over .env)
cli: dict = {}
if args.url is not None:
cli.setdefault("agent", {})["server_url"] = args.url
if args.base_url is not None:
cli.setdefault("llm", {})["base_url"] = args.base_url
if args.model is not None:
cli.setdefault("llm", {})["model"] = args.model
if args.api_key is not None:
cli.setdefault("llm", {})["api_key"] = args.api_key
if args.max_turns is not None:
cli.setdefault("agent", {})["max_turns"] = args.max_turns
if args.max_time is not None:
cli.setdefault("agent", {})["max_time_s"] = args.max_time
if args.verbose:
cli.setdefault("agent", {})["verbose"] = True
if args.log_file is not None:
cli.setdefault("agent", {})["log_file"] = args.log_file
if args.system_prompt is not None:
cli.setdefault("agent", {})["system_prompt_file"] = args.system_prompt
config = load_config(config_path=args.config, cli_overrides=cli)
verbose = config.agent.verbose
# Set up logging to file if requested — tee all print() to both stdout and file
if config.agent.log_file:
import builtins
_builtin_print = builtins.print
_log_fh = open(config.agent.log_file, "w", encoding="utf-8")
def _tee_print(*pargs, **kwargs):
_builtin_print(*pargs, **kwargs)
kwargs.pop("file", None)
_builtin_print(*pargs, file=_log_fh, **kwargs)
_log_fh.flush()
builtins.print = _tee_print
# API key validation: only required for remote endpoints
is_local = any(h in config.llm.base_url for h in ("localhost", "127.0.0.1", "0.0.0.0"))
if not config.llm.api_key and not is_local:
print("Error: API key required for remote LLM endpoints.")
print(" Set OPENROUTER_API_KEY or LLM_API_KEY environment variable, use --api-key,")
print(" or use a config file with llm.api_key set.")
print(" For local models (Ollama, LM Studio), use --base-url http://localhost:...")
sys.exit(1)
try:
asyncio.run(run_agent(config, verbose))
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
except ConnectionRefusedError:
print(f"\nCould not connect to {config.agent.server_url}")
print("Is the OpenRA-RL server running?")
print(" docker run -p 8000:8000 openra-rl")
sys.exit(1)
if __name__ == "__main__":
main()