Bot and Troll launchers
This commit is contained in:
parent
327bfb43ab
commit
65f76a00ac
15
AGENTS.md
15
AGENTS.md
|
|
@ -31,6 +31,11 @@ botWebWars/
|
|||
├── README.md # Human-facing overview and quickstart
|
||||
├── Dockerfile # Multi-stage production container build (frontend + backend)
|
||||
├── docker-compose.yml # Single-service container composition on port 8000
|
||||
├── launch_bots.py # Top-level batch launcher for botagent_ai instances
|
||||
├── launch_bots.sh # Shell wrapper for launch_bots.py
|
||||
├── launch_trolls.py # Top-level batch launcher for trollagent_ai instances
|
||||
├── launch_trolls.sh # Shell wrapper for launch_trolls.py
|
||||
├── launcher_common.py # Shared agent process orchestrator and log multiplexer
|
||||
│
|
||||
├── backend/ # FastAPI application & game engine
|
||||
│ ├── app/
|
||||
|
|
@ -278,6 +283,11 @@ The application is containerized into a single unified image via [Dockerfile](Do
|
|||
export OLLAMA_MODEL="gemma4:12b"
|
||||
python3 botagent_ai/bot.py -n MyAIBot -s 4 -H 10 -c "#8b5cf6"
|
||||
```
|
||||
- **Batch Multi-Session Launch (Top-Level Script)**:
|
||||
```bash
|
||||
# Launch 3 bots (named botagent_ai_gemma4_e4b_1, botagent_ai_gemma4_e4b_2, ...)
|
||||
./launch_bots.sh -n 3 -s 2 -H 2 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
```
|
||||
|
||||
### C. Vertex AI (Gemini) Bot: `botagent_gear/`
|
||||
- **Entry File**: `botagent_gear/bot.py`
|
||||
|
|
@ -329,6 +339,11 @@ The application is containerized into a single unified image via [Dockerfile](Do
|
|||
export OLLAMA_MODEL="gemma4:12b"
|
||||
python3 trollagent_ai/bot.py -n CarnageTroll -s 4 -H 10 -c "#15803d"
|
||||
```
|
||||
- **Batch Multi-Session Launch (Top-Level Script)**:
|
||||
```bash
|
||||
# Launch 2 trolls (named trollagent_ai_gemma4_e4b_1, trollagent_ai_gemma4_e4b_2)
|
||||
./launch_trolls.sh -n 2 -s 2 -H 2 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
```
|
||||
|
||||
### F. Vertex AI (Gemini) Troll Bot: `trollagent_gear/`
|
||||
- **Entry File**: `trollagent_gear/bot.py`
|
||||
|
|
|
|||
43
README.md
43
README.md
|
|
@ -124,3 +124,46 @@ $ python troll_agent.py --url "http://localhost:8000" --name "Fat Troll" --color
|
|||
```
|
||||
|
||||
BotAgents work the same way
|
||||
|
||||
---
|
||||
|
||||
## Multi-Agent Batch Launchers (Top-Level Scripts)
|
||||
|
||||
Instead of opening multiple terminal tabs manually, you can launch pools of AI bots or trolls using the top-level launcher scripts. The scripts automatically name agents as `{folder}_{sanitized_model}_{number}` (e.g. `botagent_ai_gemma4_e4b_1`, `botagent_ai_gemma4_e4b_2`, etc.), auto-assign distinct colors, multiplex real-time colorized logs in one terminal, save individual log files under `logs/`, and gracefully clean up all sessions when interrupted (`Ctrl+C`).
|
||||
|
||||
### Launch Multiple AI Bots (`botagent_ai`)
|
||||
```bash
|
||||
# Launch 3 AI bots with default parameters (gemma4:e4b, str 2, hp 2)
|
||||
./launch_bots.sh -n 3
|
||||
|
||||
# Or with python directly, specifying custom parameters:
|
||||
python3 launch_bots.py -n 4 -s 2 -H 2 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
|
||||
# Preview commands without executing
|
||||
./launch_bots.py -n 3 --dry-run
|
||||
```
|
||||
|
||||
### Launch Multiple AI Trolls (`trollagent_ai`)
|
||||
```bash
|
||||
# Launch 2 AI trolls with default parameters (gemma4:e4b, str 2, hp 2)
|
||||
./launch_trolls.sh -n 2
|
||||
|
||||
# Or with python directly, specifying custom parameters:
|
||||
python3 launch_trolls.py -n 2 -s 3 -H 4 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
|
||||
# Preview commands without executing
|
||||
./launch_trolls.py -n 2 --dry-run
|
||||
```
|
||||
|
||||
### CLI Options Supported:
|
||||
- `-n`, `--count`, `--num`: Number of agents to launch (default: `1`).
|
||||
- `-s`, `--strength`: Strength multiplier (default: `2`).
|
||||
- `-H`, `--health`: Starting health points (default: `2`).
|
||||
- `--ollama-url`: Ollama API base URL (default: `http://192.168.1.220:11434`).
|
||||
- `-m`, `--model`, `--ollama-model`: Ollama model name (default: `gemma4:e4b`).
|
||||
- `-u`, `--url`, `--server-url`: botWebWars backend URL (default: `http://localhost:8000`).
|
||||
- `-c`, `--color`: Custom avatar hex color (default: auto-cycles vibrant palette).
|
||||
- `--start-index`: Starting number index (e.g. `--start-index 4` for instances `_4`, `_5`, ...).
|
||||
- `--dry-run`: Display all commands without launching.
|
||||
- `--log-dir`: Directory for per-agent log files (default: `logs/`).
|
||||
- `--no-logs`: Disable writing log files to disk.
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Launch a specified number of AI Bot Agent (botagent_ai) sessions for botWebWars.
|
||||
|
||||
Example usage:
|
||||
# Launch 3 bots with default parameters (gemma4:e4b, str 2, hp 2)
|
||||
./launch_bots.py -n 3
|
||||
|
||||
# Custom model, health, strength, and Ollama server
|
||||
./launch_bots.py -n 2 -s 3 -H 4 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
|
||||
# Preview commands without executing
|
||||
./launch_bots.py -n 3 --dry-run
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure workspace root is in sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from launcher_common import (
|
||||
DEFAULT_BOT_COLORS,
|
||||
run_agent_launcher,
|
||||
)
|
||||
|
||||
DEFAULT_SERVER_URL = os.getenv("BOT_SERVER_URL", "http://localhost:8000")
|
||||
DEFAULT_OLLAMA_URL = os.getenv("OLLAMA_BASE_URL", "http://192.168.1.220:11434")
|
||||
DEFAULT_OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma4:e4b")
|
||||
DEFAULT_BOT_STRENGTH = int(os.getenv("BOT_STRENGTH", "2"))
|
||||
DEFAULT_BOT_HEALTH = int(os.getenv("BOT_HEALTH", "2"))
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Launch a specified number of AI Bot Agent (botagent_ai) sessions for botWebWars.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--count", "--num", "--num-bots",
|
||||
dest="count",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of bot agent instances to launch",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--strength",
|
||||
dest="strength",
|
||||
type=int,
|
||||
default=DEFAULT_BOT_STRENGTH,
|
||||
help="Bot strength multiplier (1-10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H", "--health",
|
||||
dest="health",
|
||||
type=int,
|
||||
default=DEFAULT_BOT_HEALTH,
|
||||
help="Starting health points",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ollama-url",
|
||||
dest="ollama_url",
|
||||
default=DEFAULT_OLLAMA_URL,
|
||||
help="Ollama API base URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--model", "--ollama-model",
|
||||
dest="model",
|
||||
default=DEFAULT_OLLAMA_MODEL,
|
||||
help="Ollama model identifier",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--url", "--server-url",
|
||||
dest="server_url",
|
||||
default=DEFAULT_SERVER_URL,
|
||||
help="botWebWars server URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--color",
|
||||
dest="color",
|
||||
default=None,
|
||||
help="Custom hex color code for avatar (default: auto-cycles vibrant palette)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-index",
|
||||
dest="start_index",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Starting index number for bot naming (e.g. start at 4 for botagent_ai_model_4)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix",
|
||||
dest="prefix",
|
||||
default="botagent_ai",
|
||||
help="Custom naming prefix before model and index",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--piece-type",
|
||||
dest="piece_type",
|
||||
choices=["knight", "warrior"],
|
||||
default=None,
|
||||
help="Optional board piece class",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-dir",
|
||||
dest="log_dir",
|
||||
default="logs",
|
||||
help="Directory to store per-bot log files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-logs",
|
||||
dest="no_logs",
|
||||
action="store_true",
|
||||
help="Disable writing logs to files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--python",
|
||||
dest="python",
|
||||
default=None,
|
||||
help="Custom path to python interpreter binary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
help="Print the launch plan and commands without executing them",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def extra_args_builder(args):
|
||||
extra = []
|
||||
if args.piece_type:
|
||||
extra.extend(["--piece-type", args.piece_type])
|
||||
return extra
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
run_agent_launcher(
|
||||
agent_type="Bot",
|
||||
folder="botagent_ai",
|
||||
palette=DEFAULT_BOT_COLORS,
|
||||
args=args,
|
||||
extra_args_builder=extra_args_builder,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Shell wrapper for launch_bots.py
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec python3 "${SCRIPT_DIR}/launch_bots.py" "$@"
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Launch a specified number of AI Troll Agent (trollagent_ai) sessions for botWebWars.
|
||||
|
||||
Example usage:
|
||||
# Launch 2 trolls with default parameters (gemma4:e4b, str 2, hp 2)
|
||||
./launch_trolls.py -n 2
|
||||
|
||||
# Custom strength, health, model, and Ollama server
|
||||
./launch_trolls.py -n 3 -s 3 -H 4 --ollama-url "http://192.168.1.220:11434" --model "gemma4:e4b"
|
||||
|
||||
# Preview commands without executing
|
||||
./launch_trolls.py -n 2 --dry-run
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure workspace root is in sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from launcher_common import (
|
||||
DEFAULT_TROLL_COLORS,
|
||||
run_agent_launcher,
|
||||
)
|
||||
|
||||
DEFAULT_SERVER_URL = os.getenv("TROLL_SERVER_URL", os.getenv("BOT_SERVER_URL", "http://localhost:8000"))
|
||||
DEFAULT_OLLAMA_URL = os.getenv("OLLAMA_BASE_URL", "http://192.168.1.220:11434")
|
||||
DEFAULT_OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma4:e4b")
|
||||
DEFAULT_TROLL_STRENGTH = float(os.getenv("TROLL_STRENGTH", "2.0"))
|
||||
DEFAULT_TROLL_HEALTH = float(os.getenv("TROLL_HEALTH", "2.0"))
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Launch a specified number of AI Troll Agent (trollagent_ai) sessions for botWebWars.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--count", "--num", "--num-trolls",
|
||||
dest="count",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of troll agent instances to launch",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--strength",
|
||||
dest="strength",
|
||||
type=float,
|
||||
default=DEFAULT_TROLL_STRENGTH,
|
||||
help="Troll strength multiplier (1-10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H", "--health",
|
||||
dest="health",
|
||||
type=float,
|
||||
default=DEFAULT_TROLL_HEALTH,
|
||||
help="Starting health points",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ollama-url",
|
||||
dest="ollama_url",
|
||||
default=DEFAULT_OLLAMA_URL,
|
||||
help="Ollama API base URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--model", "--ollama-model",
|
||||
dest="model",
|
||||
default=DEFAULT_OLLAMA_MODEL,
|
||||
help="Ollama model identifier",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--url", "--server-url",
|
||||
dest="server_url",
|
||||
default=DEFAULT_SERVER_URL,
|
||||
help="botWebWars server URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--color",
|
||||
dest="color",
|
||||
default=None,
|
||||
help="Custom hex color code for avatar (default: auto-cycles troll green palette)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-index",
|
||||
dest="start_index",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Starting index number for troll naming (e.g. start at 3 for trollagent_ai_model_3)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix",
|
||||
dest="prefix",
|
||||
default="trollagent_ai",
|
||||
help="Custom naming prefix before model and index",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--loop-delay",
|
||||
dest="loop_delay",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Turn polling interval in seconds",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-dir",
|
||||
dest="log_dir",
|
||||
default="logs",
|
||||
help="Directory to store per-troll log files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-logs",
|
||||
dest="no_logs",
|
||||
action="store_true",
|
||||
help="Disable writing logs to files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--python",
|
||||
dest="python",
|
||||
default=None,
|
||||
help="Custom path to python interpreter binary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
help="Print the launch plan and commands without executing them",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def extra_args_builder(args):
|
||||
extra = []
|
||||
if args.loop_delay:
|
||||
extra.extend(["--loop-delay", str(args.loop_delay)])
|
||||
return extra
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
run_agent_launcher(
|
||||
agent_type="Troll",
|
||||
folder="trollagent_ai",
|
||||
palette=DEFAULT_TROLL_COLORS,
|
||||
args=args,
|
||||
extra_args_builder=extra_args_builder,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Shell wrapper for launch_trolls.py
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec python3 "${SCRIPT_DIR}/launch_trolls.py" "$@"
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
"""Common process management and launcher utilities for botWebWars agents."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
# ANSI color codes for pretty terminal logging
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
GREEN = "\033[1;32m"
|
||||
YELLOW = "\033[1;33m"
|
||||
RED = "\033[1;31m"
|
||||
CYAN = "\033[1;36m"
|
||||
MAGENTA = "\033[1;35m"
|
||||
BLUE = "\033[1;34m"
|
||||
|
||||
CONSOLE_COLORS = [
|
||||
"\033[1;34m", # Blue
|
||||
"\033[1;35m", # Magenta
|
||||
"\033[1;36m", # Cyan
|
||||
"\033[1;32m", # Green
|
||||
"\033[1;33m", # Yellow
|
||||
"\033[1;31m", # Red
|
||||
"\033[1;94m", # Light Blue
|
||||
"\033[1;95m", # Light Magenta
|
||||
"\033[1;96m", # Light Cyan
|
||||
"\033[1;92m", # Light Green
|
||||
]
|
||||
|
||||
DEFAULT_BOT_COLORS = [
|
||||
"#3b82f6", # Blue
|
||||
"#8b5cf6", # Purple
|
||||
"#ec4899", # Pink
|
||||
"#06b6d4", # Cyan
|
||||
"#f59e0b", # Amber
|
||||
"#10b981", # Emerald
|
||||
"#6366f1", # Indigo
|
||||
"#f43f5e", # Rose
|
||||
"#14b8a6", # Teal
|
||||
"#e11d48", # Crimson
|
||||
]
|
||||
|
||||
DEFAULT_TROLL_COLORS = [
|
||||
"#15803d", # Forest Green
|
||||
"#166534", # Dark Green
|
||||
"#047857", # Emerald / Swamp
|
||||
"#4d7c0f", # Olive Lime
|
||||
"#3f6212", # Deep Olive
|
||||
"#b45309", # Dark Amber / Brown
|
||||
"#7c2d12", # Rust / Bloodwood
|
||||
"#581c87", # Dark Purple
|
||||
"#0f766e", # Deep Teal
|
||||
"#1e293b", # Slate Dark
|
||||
]
|
||||
|
||||
|
||||
def sanitize_model_name(model: str) -> str:
|
||||
"""Sanitizes model name by replacing non-alphanumeric characters with underscores.
|
||||
|
||||
Example: 'gemma4:e4b' -> 'gemma4_e4b', 'llama3.2:1b' -> 'llama3_2_1b'
|
||||
"""
|
||||
cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", model).strip("_")
|
||||
return cleaned or "model"
|
||||
|
||||
|
||||
def make_agent_name(folder: str, model: str, index: int, prefix: Optional[str] = None) -> str:
|
||||
"""Generates the standardized agent name in the format: {folder}_{model}_{index}.
|
||||
|
||||
Example: 'botagent_ai_gemma4_e4b_3'
|
||||
"""
|
||||
base_prefix = prefix.strip("_") if prefix else folder
|
||||
clean_model = sanitize_model_name(model)
|
||||
return f"{base_prefix}_{clean_model}_{index}"
|
||||
|
||||
|
||||
def find_python_executable(agent_dir: Path, custom_python: Optional[str] = None) -> str:
|
||||
"""Locates the appropriate Python binary:
|
||||
1. Custom explicit python path if provided.
|
||||
2. Active virtual environment if invoked inside one.
|
||||
3. Agent directory local virtual environment (e.g. {agent_dir}/venv/bin/python).
|
||||
4. sys.executable or system python3.
|
||||
"""
|
||||
if custom_python:
|
||||
return custom_python
|
||||
|
||||
# If the user explicitly activated an environment outside
|
||||
if getattr(sys, "base_prefix", None) != sys.prefix:
|
||||
return sys.executable
|
||||
|
||||
# Check local venv inside the agent's folder
|
||||
local_venv = agent_dir / "venv" / "bin" / "python"
|
||||
if local_venv.is_file() and os.access(local_venv, os.X_OK):
|
||||
return str(local_venv)
|
||||
|
||||
# Fallback to current sys.executable
|
||||
return sys.executable
|
||||
|
||||
|
||||
def stream_output(
|
||||
agent_name: str,
|
||||
color_code: str,
|
||||
proc: subprocess.Popen,
|
||||
log_file: Optional[Path],
|
||||
) -> None:
|
||||
"""Streams child process stdout/stderr line-by-line to console and log file."""
|
||||
log_fp = None
|
||||
if log_file:
|
||||
try:
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_fp = open(log_file, "a", encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"{RED}⚠️ Failed to open log file {log_file}: {e}{RESET}", flush=True)
|
||||
|
||||
last_lobby_time = 0.0
|
||||
try:
|
||||
if proc.stdout:
|
||||
for raw_line in iter(proc.stdout.readline, ""):
|
||||
if not raw_line:
|
||||
break
|
||||
if log_fp:
|
||||
log_fp.write(raw_line)
|
||||
log_fp.flush()
|
||||
|
||||
line = raw_line.rstrip("\r\n")
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Throttle lobby polling notifications to avoid console flooding
|
||||
if "[LOBBY]" in line:
|
||||
now = time.time()
|
||||
if now - last_lobby_time < 8.0:
|
||||
continue
|
||||
last_lobby_time = now
|
||||
|
||||
print(f"{color_code}[{agent_name}]{RESET} {line}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if log_fp:
|
||||
try:
|
||||
log_fp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def stop_all_processes(procs: List[Tuple[str, subprocess.Popen]]) -> None:
|
||||
"""Gracefully terminates all child processes by sending SIGINT first."""
|
||||
if not procs:
|
||||
return
|
||||
|
||||
running_procs = [(name, p) for name, p in procs if p.poll() is None]
|
||||
if not running_procs:
|
||||
return
|
||||
|
||||
print(f"\n{YELLOW}🛑 Stopping {len(running_procs)} agent session(s) gracefully...{RESET}", flush=True)
|
||||
|
||||
# 1. Send SIGINT so bot.py catches KeyboardInterrupt and cleanly deregisters from the board
|
||||
for name, proc in running_procs:
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.send_signal(signal.SIGINT)
|
||||
except (ProcessLookupError, OSError):
|
||||
pass
|
||||
|
||||
# 2. Give processes up to 4 seconds to deregister and exit cleanly
|
||||
deadline = time.time() + 4.0
|
||||
for name, proc in running_procs:
|
||||
remaining = max(0.1, deadline - time.time())
|
||||
try:
|
||||
proc.wait(timeout=remaining)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
# 3. Terminate/kill any processes that hung
|
||||
for name, proc in running_procs:
|
||||
if proc.poll() is None:
|
||||
print(f"{RED}⚠️ Agent {name} did not exit in time; terminating...{RESET}", flush=True)
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=1.0)
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"{GREEN}✅ All agent sessions stopped cleanly.{RESET}\n", flush=True)
|
||||
|
||||
|
||||
def run_agent_launcher(
|
||||
agent_type: str,
|
||||
folder: str,
|
||||
palette: List[str],
|
||||
args: argparse.Namespace,
|
||||
extra_args_builder=None,
|
||||
) -> None:
|
||||
"""Generic orchestrator for launching bot and troll agent pools."""
|
||||
root_dir = Path(__file__).resolve().parent
|
||||
agent_dir = root_dir / folder
|
||||
|
||||
if not agent_dir.is_dir():
|
||||
print(f"{RED}❌ Error: Agent directory '{agent_dir}' not found.{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
entry_file = agent_dir / "bot.py"
|
||||
if not entry_file.is_file():
|
||||
print(f"{RED}❌ Error: Entrypoint '{entry_file}' not found.{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
python_bin = find_python_executable(agent_dir, args.python)
|
||||
count = max(1, args.count)
|
||||
start_index = max(1, args.start_index)
|
||||
prefix = getattr(args, "prefix", None)
|
||||
|
||||
# Prepare list of commands and configurations
|
||||
agent_configs = []
|
||||
for offset in range(count):
|
||||
idx = start_index + offset
|
||||
name = make_agent_name(folder=folder, model=args.model, index=idx, prefix=prefix)
|
||||
color = args.color if args.color else palette[offset % len(palette)]
|
||||
console_color = CONSOLE_COLORS[offset % len(CONSOLE_COLORS)]
|
||||
|
||||
cmd = [
|
||||
python_bin,
|
||||
"bot.py",
|
||||
"--name", name,
|
||||
"--color", color,
|
||||
"-s", str(args.strength),
|
||||
"-H", str(args.health),
|
||||
"--ollama-url", args.ollama_url,
|
||||
"--ollama-model", args.model,
|
||||
"-u", args.server_url,
|
||||
]
|
||||
|
||||
if extra_args_builder:
|
||||
extra = extra_args_builder(args)
|
||||
if extra:
|
||||
cmd.extend(extra)
|
||||
|
||||
log_file = None
|
||||
if not args.no_logs and args.log_dir:
|
||||
log_file = Path(args.log_dir).resolve() / f"{name}.log"
|
||||
|
||||
agent_configs.append({
|
||||
"index": idx,
|
||||
"name": name,
|
||||
"color": color,
|
||||
"console_color": console_color,
|
||||
"cmd": cmd,
|
||||
"log_file": log_file,
|
||||
})
|
||||
|
||||
# Dry-run display mode
|
||||
if args.dry_run:
|
||||
print(f"\n{CYAN}{'=' * 68}{RESET}")
|
||||
print(f"{BOLD}🔍 DRY RUN: {count} {agent_type}(s) [{folder}]{RESET}")
|
||||
print(f"{CYAN}{'=' * 68}{RESET}")
|
||||
print(f"Model: {BOLD}{args.model}{RESET}")
|
||||
print(f"Ollama URL: {args.ollama_url}")
|
||||
print(f"Server URL: {args.server_url}")
|
||||
print(f"Strength: {args.strength} | Health: {args.health}")
|
||||
print(f"Interpreter: {python_bin}")
|
||||
print(f"Working Dir: {agent_dir}")
|
||||
if not args.no_logs and args.log_dir:
|
||||
print(f"Log Dir: {Path(args.log_dir).resolve()}")
|
||||
print(f"{CYAN}{'-' * 68}{RESET}")
|
||||
|
||||
for item in agent_configs:
|
||||
cmd_str = " ".join(item["cmd"])
|
||||
print(f"[{item['index']}] Name: {BOLD}{item['name']}{RESET}")
|
||||
print(f" Color: {item['color']}")
|
||||
print(f" Cmd: {cmd_str}\n")
|
||||
print(f"{CYAN}{'=' * 68}{RESET}\n")
|
||||
return
|
||||
|
||||
# Normal execution
|
||||
print(f"\n{CYAN}{'=' * 68}{RESET}")
|
||||
print(f"{BOLD}🚀 Launching {count} {agent_type}(s) [{folder}]{RESET}")
|
||||
print(f"Model: {BOLD}{args.model}{RESET}")
|
||||
print(f"Ollama URL: {args.ollama_url}")
|
||||
print(f"Server URL: {args.server_url}")
|
||||
print(f"Strength: {args.strength} | Health: {args.health}")
|
||||
print(f"Interpreter: {python_bin}")
|
||||
if not args.no_logs and args.log_dir:
|
||||
print(f"Logs: {Path(args.log_dir).resolve()}/<agent_name>.log")
|
||||
print(f"{CYAN}{'=' * 68}{RESET}")
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
env["BOT_SERVER_URL"] = args.server_url
|
||||
if "troll" in folder.lower():
|
||||
env["TROLL_SERVER_URL"] = args.server_url
|
||||
|
||||
procs: List[Tuple[str, subprocess.Popen]] = []
|
||||
threads: List[threading.Thread] = []
|
||||
|
||||
interrupted = threading.Event()
|
||||
|
||||
def handle_signal(signum, frame):
|
||||
interrupted.set()
|
||||
|
||||
# Register signal traps
|
||||
old_sigint = signal.signal(signal.SIGINT, handle_signal)
|
||||
old_sigterm = signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
try:
|
||||
for item in agent_configs:
|
||||
print(f"✨ Spawning {item['console_color']}{item['name']}{RESET} ({item['color']})...")
|
||||
proc = subprocess.Popen(
|
||||
item["cmd"],
|
||||
cwd=str(agent_dir),
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
procs.append((item["name"], proc))
|
||||
|
||||
t = threading.Thread(
|
||||
target=stream_output,
|
||||
args=(item["name"], item["console_color"], proc, item["log_file"]),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
time.sleep(0.15) # Slight stagger for clean initial connections
|
||||
|
||||
print(f"{CYAN}{'=' * 68}{RESET}")
|
||||
print(f"{BOLD}💡 All {len(procs)} agent(s) spawned. Press Ctrl+C at any time to stop.{RESET}")
|
||||
print(f"{CYAN}{'=' * 68}{RESET}\n")
|
||||
|
||||
# Monitor loop
|
||||
while not interrupted.is_set():
|
||||
# Check if all processes have exited naturally
|
||||
if all(p.poll() is not None for _, p in procs):
|
||||
break
|
||||
interrupted.wait(timeout=0.5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
interrupted.set()
|
||||
finally:
|
||||
stop_all_processes(procs)
|
||||
signal.signal(signal.SIGINT, old_sigint)
|
||||
signal.signal(signal.SIGTERM, old_sigterm)
|
||||
Loading…
Reference in New Issue