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
|
├── README.md # Human-facing overview and quickstart
|
||||||
├── Dockerfile # Multi-stage production container build (frontend + backend)
|
├── Dockerfile # Multi-stage production container build (frontend + backend)
|
||||||
├── docker-compose.yml # Single-service container composition on port 8000
|
├── 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
|
├── backend/ # FastAPI application & game engine
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
|
|
@ -278,6 +283,11 @@ The application is containerized into a single unified image via [Dockerfile](Do
|
||||||
export OLLAMA_MODEL="gemma4:12b"
|
export OLLAMA_MODEL="gemma4:12b"
|
||||||
python3 botagent_ai/bot.py -n MyAIBot -s 4 -H 10 -c "#8b5cf6"
|
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/`
|
### C. Vertex AI (Gemini) Bot: `botagent_gear/`
|
||||||
- **Entry File**: `botagent_gear/bot.py`
|
- **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"
|
export OLLAMA_MODEL="gemma4:12b"
|
||||||
python3 trollagent_ai/bot.py -n CarnageTroll -s 4 -H 10 -c "#15803d"
|
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/`
|
### F. Vertex AI (Gemini) Troll Bot: `trollagent_gear/`
|
||||||
- **Entry File**: `trollagent_gear/bot.py`
|
- **Entry File**: `trollagent_gear/bot.py`
|
||||||
|
|
|
||||||
45
README.md
45
README.md
|
|
@ -123,4 +123,47 @@ $ pip install -r requirements.txt
|
||||||
$ python troll_agent.py --url "http://localhost:8000" --name "Fat Troll" --color "#10B981" -s 2 -H 2
|
$ python troll_agent.py --url "http://localhost:8000" --name "Fat Troll" --color "#10B981" -s 2 -H 2
|
||||||
```
|
```
|
||||||
|
|
||||||
BotAgents work the same way
|
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