File size: 5,720 Bytes
02f4a63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/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()