add GEAR, add AGENTS.md
This commit is contained in:
parent
1635cfe8fd
commit
80200ca1f3
|
|
@ -0,0 +1,290 @@
|
|||
# AGENTS.md - botWebWars AI Agent Guide
|
||||
|
||||
Welcome to **botWebWars**! This document provides architectural context, component specifications, game rule references, and developer workflows for GenAI coding agents (and human developers) working in this repository.
|
||||
|
||||
---
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
**botWebWars** is an autonomous tactical bot arena built on a **64 × 64 discrete grid** (coordinates `(0, 0)` to `(64, 64)`). Bots register with custom names, colors, and strength values, explore the terrain, negotiate party alliances based on relative strength, maintain squad formations, and battle opposing parties in **3-bout D20 tactical confrontations**.
|
||||
|
||||
### Tech Stack
|
||||
- **Backend**: Python 3.12, FastAPI, Uvicorn, Pydantic v2, WebSockets, Pytest.
|
||||
- **Frontend**: React 19, TypeScript, Vite, Tailwind CSS v4, Lucide React icons.
|
||||
- **Deployment**: Multi-stage Dockerfile (Node 22 build -> Python 3.12-slim runtime), Docker Compose.
|
||||
- **Bots**:
|
||||
- `botagent`: Python heuristic agent (radar scanning, frontier exploration, deterministic leader negotiation).
|
||||
- `botagent_ai`: LLM-assisted Python agent (Ollama / LLM-driven strategic negotiation and pathing).
|
||||
- `botagent_gear`: Vertex AI (Gemini) LLM-assisted agent (Google Cloud Vertex AI / Gemini 2.5 & 1.5 strategic reasoning).
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository Layout
|
||||
|
||||
```
|
||||
botWebWars/
|
||||
├── AGENTS.md # This file: AI agent orientation and system manual
|
||||
├── GAME_RULES.md # Canonical rules of engagement, movement, and scoring
|
||||
├── 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
|
||||
│
|
||||
├── backend/ # FastAPI application & game engine
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI entry point, CORS, static SPA serving, WebSockets
|
||||
│ │ ├── game.py # Core game logic, grid state, movement, battle resolution
|
||||
│ │ ├── models.py # Pydantic schemas (Player, Party, Battle, Move, etc.)
|
||||
│ │ ├── config.py # App settings & environment configurations
|
||||
│ │ └── api/
|
||||
│ │ ├── routes.py # REST endpoints (/api/players, /api/board, /api/parties, etc.)
|
||||
│ │ └── websocket.py # Connection manager for real-time WebSocket clients
|
||||
│ ├── tests/
|
||||
│ │ └── test_api.py # Pytest test suite for game mechanics & API routes
|
||||
│ └── requirements.txt # Backend dependencies
|
||||
│
|
||||
├── frontend/ # React + Vite + TypeScript application
|
||||
│ ├── src/
|
||||
│ │ ├── App.tsx # Main UI shell, turn HUD, control panel, modals
|
||||
│ │ ├── components/ # Grid, radar, scoreboard, battle log, player controls
|
||||
│ │ ├── hooks/ # WebSocket connections and state synchronization
|
||||
│ │ └── types.ts # Frontend TypeScript interfaces mirroring backend models
|
||||
│ ├── package.json # Frontend dependencies and build scripts
|
||||
│ └── vite.config.ts # Vite build configuration
|
||||
│
|
||||
├── botagent/ # Heuristic autonomous bot
|
||||
│ ├── bot_agent.py # Autonomous client using radar, memory, and game API
|
||||
│ └── README.md # Execution command examples
|
||||
│
|
||||
├── botagent_ai/ # LLM-assisted autonomous bot (Ollama)
|
||||
│ ├── bot.py # Bot delegating strategic pathing/alliances to an LLM
|
||||
│ ├── requirements.txt # Dependencies (ollama, requests)
|
||||
│ └── INSTALL.md # Setup, Ollama configuration, and execution instructions
|
||||
│
|
||||
├── botagent_gear/ # LLM-assisted autonomous bot (Google Vertex AI / Gemini)
|
||||
│ ├── bot.py # Bot delegating decisions to Vertex AI Gemini
|
||||
│ ├── requirements.txt # Dependencies (google-auth, requests)
|
||||
│ ├── SETUP.md # Google Cloud authentication & Vertex AI setup guide
|
||||
│ ├── INSTALL.md # Installation, virtual environment, and dependency instructions
|
||||
│ └── README.md # Feature overview and invocation examples
|
||||
│
|
||||
└── examples/ # Reference screenshots and board style designs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Game Rules & Mechanics (`GAME_RULES.md` Reference)
|
||||
|
||||
When writing bots, backend engine logic, or game simulations, adhere strictly to [GAME_RULES.md](GAME_RULES.md):
|
||||
|
||||
### 1. Alliances (Solo vs. Solo)
|
||||
- When two solo bots encounter each other (adjacent or on same tile), they may form an alliance.
|
||||
- The bot with **greater strength** (or higher score if tied) becomes the **Party Leader**.
|
||||
- While alliances are voluntary, larger parties have a statistical advantage in battle.
|
||||
|
||||
### 2. Solo vs. Party Encounters
|
||||
- A solo bot **joins** an existing party if the party leader's strength is >= the solo bot's strength.
|
||||
- If the party leader is **weaker** than the solo bot, the solo bot refuses to join, and the party engages the solo bot in battle.
|
||||
|
||||
### 3. Party vs. Party Battles (3-Bout D20)
|
||||
- Two opposing parties that encounter each other **must** battle.
|
||||
- **Bout Resolution**:
|
||||
- Exactly 3 bouts are conducted.
|
||||
- In each bout, each party rolls a 20-sided die (D20 between 1 and 20).
|
||||
- Bout Score = Squad Total Strength * D20 Roll.
|
||||
- Highest score wins the bout. Best 2-out-of-3 bouts wins the overall battle.
|
||||
- **Scoring & Penalties**:
|
||||
- **Winning Leader**: +2 score points.
|
||||
- **Winning Party Members**: +1 score point each.
|
||||
- **Losing Leader**: -1 score point, stripped of leadership, removed from party, and respawned at a random open grid coordinate.
|
||||
- **Losing Party Members**: 0 score penalty; all surviving followers are **absorbed** into the winning party.
|
||||
|
||||
### 4. Party Squad Movement
|
||||
- The party leader chooses movement direction.
|
||||
- Follower members follow the leader in a single-file line (each member moves to the position vacated by the bot immediately ahead of them) maintaining Chebyshev distance <= 1.
|
||||
|
||||
### 5. Terrain Obstacles & Diagonal Squeeze
|
||||
- Mountains and forests are impassable.
|
||||
- Bots can squeeze diagonally between adjacent obstacle corners, with strength penalties:
|
||||
- **Solo bot**: -0.1 strength.
|
||||
- **Party**: Leader loses -0.2 strength; followers each lose -0.1 strength.
|
||||
- Moving along outer obstacle edges has no penalty. Minimum bot strength floor is 0.1.
|
||||
|
||||
### 6. Game Conclusion
|
||||
- The game concludes when all bots on the board are united into a **single remaining party**.
|
||||
- Final rankings/trophies (1st, 2nd, 3rd) are awarded based on **Score** (with **Strength** as the tiebreaker).
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend & API Specifications
|
||||
|
||||
The backend serves both REST endpoints under `/api` and a live WebSocket stream at `/ws`.
|
||||
|
||||
### Key REST Endpoints
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/health` | Container healthcheck endpoint |
|
||||
| `POST` | `/api/players` | Register bot: `{"name": str, "color": str, "strength": int}` |
|
||||
| `GET` | `/api/players` | List all active players, scores, and positions |
|
||||
| `GET` | `/api/board` | Full board state (grid, obstacles, players, parties, current turn) |
|
||||
| `POST` | `/api/board/reset` | Clear board, reset parties, and reset players |
|
||||
| `GET` | `/api/players/{id}/radar` | Scans surroundings, finds closest bots, identifies allies/opponents |
|
||||
| `GET` | `/api/players/{id}/memory` | Coordinate history and visited locations |
|
||||
| `GET` | `/api/players/{id}/available-moves` | Valid movements in all 8 directions (evaluates terrain & obstacles) |
|
||||
| `POST` | `/api/players/{id}/move` | Execute a move (`{"direction": "N"|"S"|"E"|"W"|"NE"|"NW"|"SE"|"SW"}`) |
|
||||
| `POST` | `/api/players/{id}/ai-step` | Perform one autonomous decision step via internal game engine |
|
||||
| `POST` | `/api/players/{id}/pass` | Pass turn to next queued entity |
|
||||
| `GET` | `/api/turn` | Current turn owner, round number, and turn order |
|
||||
| `GET` | `/api/parties` | List all parties, leaders, and member IDs |
|
||||
| `POST` | `/api/parties` | Manually construct an alliance party |
|
||||
| `POST` | `/api/battles/fight` | Initiate a 3-bout D20 battle between adjacent parties |
|
||||
|
||||
### Real-Time WebSocket (`/ws`)
|
||||
- Automatically broadcasts state changes (`init`, `move`, `battle`, `party_formed`, `turn_change`, `game_over`).
|
||||
- Client can send ping messages `{"action": "ping"}` and receives `{"event": "pong"}`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend Architecture
|
||||
|
||||
- **Path**: `frontend/`
|
||||
- **Framework**: React 19 + TypeScript bundled with Vite.
|
||||
- **Styling**: Tailwind CSS v4.
|
||||
- **Key Features**:
|
||||
- Real-time 64x64 board renderer with zoom/pan and custom obstacle textures.
|
||||
- Live turn indicators, active player highlight, and party squad link lines.
|
||||
- Radar visualization panel showing target distances and alignment.
|
||||
- Battle modal showing animated D20 rolls, multipliers, bout winners, and score adjustments.
|
||||
- Autonomous loop toggle to run rounds continuously from the browser.
|
||||
|
||||
---
|
||||
|
||||
## 6. Docker & Container Deployment
|
||||
|
||||
The application is containerized into a single unified image via [Dockerfile](Dockerfile) and [docker-compose.yml](docker-compose.yml):
|
||||
|
||||
1. **Stage 1 (`frontend-builder`)**:
|
||||
- Base image: `node:22-alpine`
|
||||
- Installs packages and runs `npm run build` to generate `frontend/dist`.
|
||||
2. **Stage 2 (`production`)**:
|
||||
- Base image: `python:3.12-slim`
|
||||
- Installs backend requirements from `backend/requirements.txt`.
|
||||
- Copies `backend/` source and static assets from `frontend/dist` into `frontend/dist`.
|
||||
- FastAPI serves static assets at `/` and `/assets` while mounting API routes at `/api` and `/ws`.
|
||||
- Built-in container healthcheck calls `curl -f http://localhost:8000/api/health`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bot Implementations
|
||||
|
||||
### A. Heuristic Bot: `botagent/`
|
||||
- **Entry File**: `botagent/bot_agent.py`
|
||||
- **Technique**: Deterministic rule-based autonomous agent.
|
||||
- **Workflow**:
|
||||
1. Registers bot via `POST /api/players`.
|
||||
2. Polls `GET /api/turn` to wait for its turn.
|
||||
3. Uses `GET /api/players/{id}/radar` to find nearest target.
|
||||
4. Avoids looping using internal coordinate history.
|
||||
5. Computes vector direction, evaluates diagonal obstacles, and moves.
|
||||
6. Evaluates alliances vs. fights strictly according to strength hierarchy.
|
||||
- **Run Command**:
|
||||
```bash
|
||||
python3 botagent/bot_agent.py --name CyberBot --color "#10b981" -s 4 --url http://localhost:8000/api
|
||||
```
|
||||
|
||||
### B. LLM-Assisted Bot: `botagent_ai/`
|
||||
- **Entry File**: `botagent_ai/bot.py`
|
||||
- **Technique**: LLM strategic decision maker with deterministic rule safeguards.
|
||||
- **LLM Integration**:
|
||||
- Communicates with an LLM backend (configured for local/remote Ollama HTTP API at `/api/generate` with model `gemma4:12b`, or adaptable to OpenAI-compatible endpoints).
|
||||
- Delegates discretionary decisions to the model:
|
||||
- Voluntary alliances (whether to ally or keep hunting when solo meets solo).
|
||||
- Navigation direction toward radar targets while balancing obstacle squeeze trade-offs.
|
||||
- Mandatory rules (forced battles, forced absorption when weaker than leader) are enforced deterministically by the game engine regardless of LLM preference.
|
||||
- **Configuration & Environment Variables**:
|
||||
- `OLLAMA_BASE_URL` / `--ollama-url`: LLM server address (default: `http://192.168.1.220:11434`).
|
||||
- `OLLAMA_MODEL` / `--ollama-model`: Model identifier (default: `gemma4:12b`).
|
||||
- `BOT_SERVER_URL` / `-u`: botWebWars backend URL (default: `http://localhost:8000/api`).
|
||||
- `BOT_NAME` / `-n`: Bot name.
|
||||
- `BOT_COLOR` / `-c`: Bot hex color.
|
||||
- `BOT_STRENGTH` / `-s`: Starting strength (1-10).
|
||||
- **Run Command**:
|
||||
```bash
|
||||
export OLLAMA_BASE_URL="http://localhost:11434"
|
||||
export OLLAMA_MODEL="gemma4:12b"
|
||||
python3 botagent_ai/bot.py -n MyAIBot -s 4 -c "#8b5cf6"
|
||||
```
|
||||
|
||||
### C. Vertex AI (Gemini) Bot: `botagent_gear/`
|
||||
- **Entry File**: `botagent_gear/bot.py`
|
||||
- **Technique**: Google Cloud Vertex AI (Gemini) model reasoning for spatial navigation, minimap analysis, and strategic voluntary alliances.
|
||||
- **LLM Integration**:
|
||||
- Communicates with Google Cloud Vertex AI generateContent REST endpoint or Google AI Studio Gemini API.
|
||||
- Features structured JSON generation (`responseMimeType: application/json`) and system instruction enforcement.
|
||||
- Supports multiple authentication methods:
|
||||
- Interactive developer login (`gcloud auth application-default login` or `gcloud auth login`).
|
||||
- Automated service account keys (`GOOGLE_APPLICATION_CREDENTIALS`).
|
||||
- Direct API keys (`GEMINI_API_KEY` or `VERTEX_API_KEY`).
|
||||
- **Documentation**:
|
||||
- [SETUP.md](botagent_gear/SETUP.md): Google Cloud authentication, project creation, API enablement, and credentials setup.
|
||||
- [INSTALL.md](botagent_gear/INSTALL.md): Virtual environment and dependency installation instructions.
|
||||
- [README.md](botagent_gear/README.md): Bot overview, CLI reference, and execution examples.
|
||||
- **Run Command**:
|
||||
```bash
|
||||
python3 botagent_gear/bot.py --name GeminiGear --color "#4285f4" -s 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Common Developer Workflows
|
||||
|
||||
### Run with Docker Compose (Recommended)
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
Access UI at `http://localhost:8000`.
|
||||
|
||||
### Local Development (Manual Setup)
|
||||
|
||||
#### 1. Backend:
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
#### 2. Frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### 3. Run Backend Test Suite:
|
||||
```bash
|
||||
# Run locally with pytest
|
||||
PYTHONPATH=backend pytest backend/tests
|
||||
|
||||
# Or run via docker container
|
||||
docker run --rm botwebwars:test pytest backend/tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Guidelines for GenAI Agents Modifying this Codebase
|
||||
|
||||
1. **Preserve Rules Fidelity**:
|
||||
- Any updates to alliance logic, battles, diagonal squeezes, or victory states must match [GAME_RULES.md](GAME_RULES.md).
|
||||
- Update `GAME_RULES.md` and tests in `backend/tests/test_api.py` whenever game mechanics are modified.
|
||||
|
||||
2. **Frontend-Backend Sync**:
|
||||
- When modifying schemas in `backend/app/models.py`, update matching TypeScript interfaces in `frontend/src/types.ts`.
|
||||
- Maintain WebSocket payload compatibility between `backend/app/api/websocket.py` and frontend hooks (`frontend/src/hooks/`).
|
||||
|
||||
3. **Bot Compatibility**:
|
||||
- Ensure REST endpoint signature modifications are reflected in `botagent/bot_agent.py`, `botagent_ai/bot.py`, and `botagent_gear/bot.py`.
|
||||
- Preserve CLI flag compatibility across all bot scripts.
|
||||
|
||||
4. **Multi-Stage Build Integrity**:
|
||||
- If adding frontend build dependencies, verify `Dockerfile` compiles cleanly in stage 1 (`npm run build`) and correctly mounts to `frontend/dist` in stage 2.
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Installation & Setup Guide for botagent_gear
|
||||
|
||||
`botagent_gear` is an autonomous botWebWars agent powered by **Google Cloud Vertex AI** and **Gemini** models.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
- **Python 3.9+** (tested on 3.10, 3.11, 3.12, and 3.14).
|
||||
- Active Google Cloud Vertex AI access or Gemini API Key (see [SETUP.md](SETUP.md)).
|
||||
- Running **botWebWars** game server (by default at `http://localhost:8000/api`).
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Navigate to the bot directory
|
||||
```bash
|
||||
cd botagent_gear
|
||||
```
|
||||
|
||||
### 2. (Recommended) Create and activate a Virtual Environment
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
*(On Windows: `venv\Scripts\activate`)*
|
||||
|
||||
### 3. Install Python Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
The dependencies are kept lightweight and robust:
|
||||
- `requests`: HTTP client for the botWebWars game API and Vertex AI REST endpoints.
|
||||
- `google-auth`: Google Cloud Application Default Credentials (ADC) and OAuth 2.0 token management.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the bot via environment variables or command-line arguments.
|
||||
|
||||
### Environment Variables
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `VERTEX_PROJECT_ID` | Auto-detected from `gcloud` | Google Cloud project ID |
|
||||
| `VERTEX_LOCATION` | `us-central1` | Vertex AI region (`us-central1`, `us-east4`, etc.) |
|
||||
| `VERTEX_MODEL` | `gemini-2.5-flash` | Model ID (`gemini-2.5-flash`, `gemini-1.5-flash`, `gemini-2.5-pro`) |
|
||||
| `GEMINI_API_KEY` / `VERTEX_API_KEY` | *(None)* | Optional API key for Google AI Studio or Vertex express mode |
|
||||
| `BOT_SERVER_URL` | `http://localhost:8000/api` | botWebWars REST API endpoint |
|
||||
| `BOT_NAME` | `GeminiGearBot` | Bot display name on the grid |
|
||||
| `BOT_COLOR` | `#4285f4` | Hex color code for the bot avatar |
|
||||
| `BOT_STRENGTH` | `5` | Starting strength (1 to 10) |
|
||||
|
||||
### CLI Options
|
||||
```text
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-u, --url SERVER_URL Backend REST API base URL (env: BOT_SERVER_URL)
|
||||
-n, --name NAME Display name for this bot (env: BOT_NAME)
|
||||
-c, --color COLOR Hex color code for the bot avatar (env: BOT_COLOR)
|
||||
-s, --strength STRENGTH
|
||||
Strength attribute (1-10) for battle multiplier (env: BOT_STRENGTH)
|
||||
-p, --project PROJECT_ID
|
||||
Google Cloud Project ID (env: VERTEX_PROJECT_ID)
|
||||
-l, --location LOCATION
|
||||
Vertex AI region / location (env: VERTEX_LOCATION)
|
||||
-m, --model MODEL Gemini model ID (env: VERTEX_MODEL)
|
||||
-k, --api-key API_KEY
|
||||
Gemini API Key or Vertex AI express mode key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
See [SETUP.md](SETUP.md) for authenticating to Google Cloud or setting up an API key, and [README.md](README.md) for quick invocation commands.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# botagent_gear: Vertex AI (Gemini) Autonomous Agent
|
||||
|
||||
**`botagent_gear`** is an intelligent, autonomous agent for **botWebWars** powered by Google Cloud's **Vertex AI** and **Gemini** foundation models (such as `gemini-2.5-flash`).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Unlike heuristic bots that use static pathing formulas, `botagent_gear` combines:
|
||||
1. **Gemini Spatial & Strategic Reasoning**:
|
||||
- Analyzes real-time radar scans, opponent strengths, party sizes, and an ASCII minimap of nearby terrain.
|
||||
- Evaluates obstacle squeezes (balancing strength penalties vs. path efficiency).
|
||||
- Decides voluntary alliances with encountered solo bots.
|
||||
2. **Deterministic Rules Enforcement**:
|
||||
- Strictly conforms to [GAME_RULES.md](../GAME_RULES.md).
|
||||
- Rule-mandated actions (forced battles, forced joins when weaker than a party leader) are executed deterministically by the game engine, while Gemini guides discretionary tactical decisions.
|
||||
3. **Multi-Authentication Support**:
|
||||
- Works with Google Cloud CLI (`gcloud auth application-default login`), service account keys (`GOOGLE_APPLICATION_CREDENTIALS`), or direct API keys (`GEMINI_API_KEY`).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Prerequisites & Installation
|
||||
Ensure you have completed the setup in [SETUP.md](SETUP.md) and [INSTALL.md](INSTALL.md):
|
||||
```bash
|
||||
cd botagent_gear
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Configure Authentication
|
||||
Choose one of the methods described in [SETUP.md](SETUP.md). For example, with `gcloud`:
|
||||
```bash
|
||||
# 1. Log in to Google Cloud
|
||||
gcloud auth login
|
||||
|
||||
# 2. Set your Google Cloud project
|
||||
gcloud config set project YOUR_GCP_PROJECT_ID
|
||||
|
||||
# 3. Authorize Application Default Credentials
|
||||
gcloud auth application-default login
|
||||
```
|
||||
|
||||
*(Alternatively, if using an API key from Google AI Studio: `export GEMINI_API_KEY="your-api-key"`).*
|
||||
|
||||
### 3. Launch the Bot
|
||||
|
||||
#### Default invocation:
|
||||
```bash
|
||||
python3 bot.py
|
||||
```
|
||||
|
||||
#### Custom Bot parameters (Name, Color, Strength, Model):
|
||||
```bash
|
||||
python3 bot.py \
|
||||
--name "GeminiTitan" \
|
||||
--color "#0ea5e9" \
|
||||
--strength 6 \
|
||||
--model "gemini-2.5-flash" \
|
||||
--project "YOUR_GCP_PROJECT_ID"
|
||||
```
|
||||
|
||||
#### Using Environment Variables:
|
||||
```bash
|
||||
export VERTEX_PROJECT_ID="YOUR_GCP_PROJECT_ID"
|
||||
export VERTEX_LOCATION="us-central1"
|
||||
export VERTEX_MODEL="gemini-2.5-flash"
|
||||
export BOT_NAME="GearBot"
|
||||
export BOT_COLOR="#10b981"
|
||||
export BOT_STRENGTH="5"
|
||||
|
||||
python3 bot.py
|
||||
```
|
||||
|
||||
#### Connecting to a Remote Game Server:
|
||||
```bash
|
||||
python3 bot.py --url "http://192.168.1.100:8000/api" --name "RemoteGear"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command-Line Arguments Reference
|
||||
|
||||
| Flag | Long Flag | Environment Variable | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `-u` | `--url` | `BOT_SERVER_URL` | `http://localhost:8000/api` | botWebWars REST API base URL |
|
||||
| `-n` | `--name` | `BOT_NAME` | `GeminiGearBot` | Display name of the bot on the grid |
|
||||
| `-c` | `--color` | `BOT_COLOR` | `#4285f4` | Hex color code for the bot avatar |
|
||||
| `-s` | `--strength` | `BOT_STRENGTH` | `5` | Starting strength (1 to 10) |
|
||||
| `-p` | `--project` | `VERTEX_PROJECT_ID` | Auto-detected from `gcloud` | Google Cloud Project ID |
|
||||
| `-l` | `--location` | `VERTEX_LOCATION` | `us-central1` | Google Cloud region for Vertex AI |
|
||||
| `-m` | `--model` | `VERTEX_MODEL` | `gemini-2.5-flash` | Gemini model name |
|
||||
| `-k` | `--api-key` | `VERTEX_API_KEY` / `GEMINI_API_KEY` | *(None)* | Optional Gemini API key |
|
||||
|
||||
---
|
||||
|
||||
## How It Works in Battle
|
||||
|
||||
1. **Lobby & Registration**:
|
||||
- The agent calls `POST /api/players` to enter the arena.
|
||||
- Waits for the game to start via the frontend UI.
|
||||
2. **Turn Polling**:
|
||||
- Polls `GET /api/turn` until it is this bot's turn.
|
||||
3. **Radar & Minimap Assessment**:
|
||||
- Obtains radar targets via `GET /api/players/{id}/radar`.
|
||||
- Computes an 8×8 ASCII minimap centered around the bot displaying obstacles (`M` mountain, `F` forest, `V` valley), allies (`A`), and enemies (`E`).
|
||||
4. **Gemini Reasoning**:
|
||||
- Prompts Gemini with legal moves, diagonal squeeze penalties, distances to targets, and current health/score.
|
||||
- Gemini returns structured JSON with the selected direction and reasoning.
|
||||
5. **Encounters & 3-Bout Confrontations**:
|
||||
- If adjacent to another bot, initiates party alliances or engages in 3-Bout D20 tactical battles via `POST /api/battles/fight`.
|
||||
6. **Game Conclusion**:
|
||||
- Detects when all bots are united under a single winning squad and announces final ranking.
|
||||
|
||||
---
|
||||
|
||||
## Running Multi-Bot Arenas
|
||||
|
||||
To experience an arena with diverse AI agents, run multiple bots in separate terminal tabs:
|
||||
|
||||
**Terminal 1 (Heuristic Bot):**
|
||||
```bash
|
||||
python3 botagent/bot_agent.py --name Heuristic1 --color "#ef4444" -s 3
|
||||
```
|
||||
|
||||
**Terminal 2 (Local Ollama LLM Bot):**
|
||||
```bash
|
||||
python3 botagent_ai/bot.py -n OllamaAgent -s 4 -c "#8b5cf6"
|
||||
```
|
||||
|
||||
**Terminal 3 (Vertex AI Gemini Bot):**
|
||||
```bash
|
||||
python3 botagent_gear/bot.py --name GeminiGear --color "#4285f4" -s 5
|
||||
```
|
||||
|
||||
Open the web interface at `http://localhost:8000`, click **Start Game**, and watch the tactical confrontations unfold live!
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# Google Cloud Vertex AI Setup Guide
|
||||
|
||||
This guide walks you through setting up Google Cloud authentication and Vertex AI access for `botagent_gear`.
|
||||
|
||||
Since you have **not authenticated to a Google Cloud project yet and have not created a key**, choose the method that best fits your workflow below.
|
||||
|
||||
---
|
||||
|
||||
## Quick Navigation
|
||||
- [Option 1: Google Cloud CLI & User Authentication (Recommended)](#option-1-google-cloud-cli--user-authentication-recommended)
|
||||
- [Option 2: Service Account Key (Headless / Automated Servers)](#option-2-service-account-key-headless--automated-servers)
|
||||
- [Option 3: Gemini API Key (Fastest Setup via Google AI Studio)](#option-3-gemini-api-key-fastest-setup-via-google-ai-studio)
|
||||
- [Verification & Troubleshooting](#verification--troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
- A Google Cloud account ([Google Cloud Free Tier](https://cloud.google.com/free) includes $300 in credits).
|
||||
- A Google Cloud Project (or permission to create one).
|
||||
|
||||
---
|
||||
|
||||
## Option 1: Google Cloud CLI & User Authentication (Recommended)
|
||||
|
||||
This is the standard, interactive developer workflow using the Google Cloud CLI (`gcloud`).
|
||||
|
||||
### Step 1: Install `gcloud` (if not installed)
|
||||
Check if `gcloud` is installed:
|
||||
```bash
|
||||
gcloud --version
|
||||
```
|
||||
If not installed, install it following [Google Cloud SDK Installation](https://cloud.google.com/sdk/docs/install) (or on Debian/Ubuntu: `sudo apt-get install google-cloud-cli`).
|
||||
|
||||
### Step 2: Log into your Google Cloud account
|
||||
```bash
|
||||
gcloud auth login
|
||||
```
|
||||
A browser window will open asking you to sign in with your Google account.
|
||||
|
||||
### Step 3: Set or Create your Project
|
||||
List existing projects:
|
||||
```bash
|
||||
gcloud projects list
|
||||
```
|
||||
If you already have a project, set it as active:
|
||||
```bash
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
```
|
||||
Or create a brand new project:
|
||||
```bash
|
||||
gcloud projects create my-botwebwars-project --name="botWebWars Project"
|
||||
gcloud config set project my-botwebwars-project
|
||||
```
|
||||
*(Ensure billing is enabled for your project in the [Google Cloud Console Billing section](https://console.cloud.google.com/billing).)*
|
||||
|
||||
### Step 4: Enable the Vertex AI API
|
||||
Run:
|
||||
```bash
|
||||
gcloud services enable aiplatform.googleapis.com
|
||||
```
|
||||
|
||||
### Step 5: Authorize Application Default Credentials (ADC)
|
||||
This allows Python scripts and SDKs to authenticate automatically:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
Follow the browser prompt to grant access.
|
||||
|
||||
### Step 6: Set Environment Variables (Optional but convenient)
|
||||
Add to your `~/.bashrc` or run in your terminal:
|
||||
```bash
|
||||
export VERTEX_PROJECT_ID=$(gcloud config get-value project)
|
||||
export VERTEX_LOCATION="us-central1"
|
||||
export VERTEX_MODEL="gemini-2.5-flash"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Service Account Key (Headless / Automated Servers)
|
||||
|
||||
If running in a Docker container, CI/CD pipeline, or remote VM without a web browser, use a Service Account:
|
||||
|
||||
### Step 1: Create a Service Account
|
||||
```bash
|
||||
export PROJECT_ID=$(gcloud config get-value project)
|
||||
|
||||
gcloud iam service-accounts create botwebwars-agent \
|
||||
--display-name="botWebWars Vertex AI Agent"
|
||||
```
|
||||
|
||||
### Step 2: Grant the Vertex AI User role
|
||||
```bash
|
||||
gcloud projects add-iam-policy-binding $PROJECT_ID \
|
||||
--member="serviceAccount:botwebwars-agent@${PROJECT_ID}.iam.gserviceaccount.com" \
|
||||
--role="roles/aiplatform.user"
|
||||
```
|
||||
|
||||
### Step 3: Create and Download the Key File
|
||||
```bash
|
||||
mkdir -p ~/.gcp
|
||||
gcloud iam service-accounts keys create ~/.gcp/vertex-key.json \
|
||||
--iam-account="botwebwars-agent@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
```
|
||||
|
||||
### Step 4: Point to the Key File
|
||||
```bash
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.gcp/vertex-key.json"
|
||||
export VERTEX_PROJECT_ID="$PROJECT_ID"
|
||||
export VERTEX_LOCATION="us-central1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option 3: Gemini API Key (Fastest Setup via Google AI Studio)
|
||||
|
||||
If you prefer using an API key without configuring GCP IAM roles or OAuth tokens:
|
||||
|
||||
1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey).
|
||||
2. Click **Create API Key**.
|
||||
3. Copy your API key.
|
||||
4. Export the key:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_API_KEY_HERE"
|
||||
```
|
||||
The `botagent_gear` agent will detect `GEMINI_API_KEY` and interact with Gemini directly.
|
||||
|
||||
---
|
||||
|
||||
## Verification & Troubleshooting
|
||||
|
||||
### 1. Test your credentials
|
||||
You can quickly verify that Vertex AI accepts your credentials:
|
||||
```bash
|
||||
python3 -c "
|
||||
import subprocess, requests, json, os
|
||||
|
||||
token = os.getenv('VERTEX_ACCESS_TOKEN') or subprocess.check_output(['gcloud', 'auth', 'print-access-token'], text=True).strip()
|
||||
project = os.getenv('VERTEX_PROJECT_ID') or subprocess.check_output(['gcloud', 'config', 'get-value', 'project'], text=True).strip()
|
||||
location = os.getenv('VERTEX_LOCATION', 'us-central1')
|
||||
model = os.getenv('VERTEX_MODEL', 'gemini-2.5-flash')
|
||||
|
||||
url = f'https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent'
|
||||
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
||||
payload = {'contents': [{'role': 'user', 'parts': [{'text': 'Hello Gemini'}]}]}
|
||||
|
||||
res = requests.post(url, headers=headers, json=payload, timeout=20)
|
||||
print('Status:', res.status_code)
|
||||
if res.status_code == 200:
|
||||
print('Vertex AI connection successful! Candidate:', res.json()['candidates'][0]['content']['parts'][0]['text'].strip())
|
||||
else:
|
||||
print('Error response:', res.text)
|
||||
"
|
||||
```
|
||||
|
||||
### 2. Common Errors
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|---|---|---|
|
||||
| `403 PermissionDenied: Vertex AI API has not been used...` | API is disabled | Run `gcloud services enable aiplatform.googleapis.com` |
|
||||
| `401 Unauthorized` / `Token expired` | Token expired or invalid | Re-run `gcloud auth application-default login` or refresh `gcloud auth login` |
|
||||
| `404 Publisher model ... not found` | Region does not have the model | Default to `us-central1`, `us-east4`, or check model name (`gemini-2.5-flash`, `gemini-1.5-flash`) |
|
||||
| `No Google Cloud project ID detected` | Project is not set | Run `gcloud config set project <PROJECT_ID>` or export `VERTEX_PROJECT_ID` |
|
||||
|
||||
Once setup is complete, proceed to [INSTALL.md](INSTALL.md) and [README.md](README.md) to install dependencies and run your agent!
|
||||
|
|
@ -0,0 +1,738 @@
|
|||
"""AI-driven Bot Agent for botWebWars, powered by Google Cloud Vertex AI (Gemini).
|
||||
|
||||
Replicates the capabilities of botagent/bot_agent.py and botagent_ai/bot.py
|
||||
(registration, radar-based navigation, obstacle avoidance, party formation, battles)
|
||||
while delegating strategic decisions to Google Vertex AI Gemini models:
|
||||
- Whether to propose a voluntary alliance with another solo bot.
|
||||
- Which direction to move towards when navigating (radar/obstacle/local map data).
|
||||
|
||||
Outcomes mandated by GAME_RULES.md (forced battles, forced joins based on
|
||||
relative strength) are always resolved deterministically by the game engine
|
||||
regardless of what the LLM prefers - the LLM is only ever offered a choice
|
||||
among legal options.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
# Optional google-auth integration
|
||||
try:
|
||||
import google.auth
|
||||
import google.auth.transport.requests
|
||||
HAVE_GOOGLE_AUTH = True
|
||||
except ImportError:
|
||||
HAVE_GOOGLE_AUTH = False
|
||||
|
||||
DEFAULT_SERVER_URL = "http://localhost:8000/api"
|
||||
DEFAULT_BOT_NAME = "GeminiGearBot"
|
||||
DEFAULT_BOT_COLOR = "#4285f4"
|
||||
DEFAULT_BOT_STRENGTH = 5
|
||||
DEFAULT_MODEL = "gemini-3.8-flash"
|
||||
DEFAULT_LOCATION = "us-central1"
|
||||
|
||||
GAME_RULES_SUMMARY = """
|
||||
Rules you must respect when choosing among the OPTIONS given to you:
|
||||
- Two solo bots that meet MAY voluntarily ally (not required). The stronger bot (or higher
|
||||
score if tied) leads. Larger parties have an advantage in battle.
|
||||
- A solo bot always joins a party if the party leader's strength >= its own (no choice).
|
||||
- A solo bot always refuses and fights if the party leader is weaker (no choice).
|
||||
- Two opposing parties that meet must always battle (no choice).
|
||||
- Diagonal squeezes between obstacle corners cost strength (0.1 solo, 0.2 leader / 0.1 followers).
|
||||
- The game ends when all bots are united into a single party.
|
||||
You will only ever be asked to choose between options that are legal - always answer with the
|
||||
requested JSON object and nothing else.
|
||||
"""
|
||||
|
||||
|
||||
def normalize_url(url: str) -> str:
|
||||
"""Ensure the API URL ends with /api without trailing slashes."""
|
||||
cleaned = url.rstrip("/")
|
||||
if not cleaned.endswith("/api"):
|
||||
cleaned = f"{cleaned}/api"
|
||||
return cleaned
|
||||
|
||||
|
||||
def extract_json(text: str) -> Optional[Dict[str, Any]]:
|
||||
"""Best-effort extraction of a JSON object from a model response."""
|
||||
if not text:
|
||||
return None
|
||||
cleaned = text.strip()
|
||||
# Remove markdown code fences if present
|
||||
if cleaned.startswith("```"):
|
||||
cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned)
|
||||
cleaned = re.sub(r"\s*```$", "", cleaned)
|
||||
match = re.search(r"\{.*\}", cleaned, re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
class VertexGeminiClient:
|
||||
"""Client for querying Gemini models on Google Cloud Vertex AI or Google AI Studio."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project_id: Optional[str] = None,
|
||||
location: str = DEFAULT_LOCATION,
|
||||
model: str = DEFAULT_MODEL,
|
||||
api_key: Optional[str] = None,
|
||||
):
|
||||
self.location = location or DEFAULT_LOCATION
|
||||
self.model = model or DEFAULT_MODEL
|
||||
self.api_key = api_key or os.getenv("VERTEX_API_KEY") or os.getenv("GEMINI_API_KEY")
|
||||
self.project_id = project_id or os.getenv("VERTEX_PROJECT_ID") or os.getenv("GCP_PROJECT") or os.getenv("GOOGLE_CLOUD_PROJECT")
|
||||
|
||||
self._cached_token: Optional[str] = None
|
||||
self._token_expiry: float = 0.0
|
||||
|
||||
# Auto-detect project if not explicitly supplied
|
||||
if not self.project_id and not self.api_key:
|
||||
self.project_id = self._detect_project()
|
||||
|
||||
def _detect_project(self) -> Optional[str]:
|
||||
"""Attempt to determine the GCP project from environment or gcloud config."""
|
||||
if HAVE_GOOGLE_AUTH:
|
||||
try:
|
||||
_, proj = google.auth.default()
|
||||
if proj:
|
||||
return proj
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if shutil.which("gcloud"):
|
||||
try:
|
||||
res = subprocess.check_output(
|
||||
["gcloud", "config", "get-value", "project"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
if res and res != "(unset)":
|
||||
return res
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _get_access_token(self) -> Optional[str]:
|
||||
"""Obtain a valid OAuth 2.0 Bearer access token for Vertex AI."""
|
||||
# 1. Direct environment variable token
|
||||
env_token = os.getenv("VERTEX_ACCESS_TOKEN") or os.getenv("GOOGLE_OAUTH_ACCESS_TOKEN")
|
||||
if env_token:
|
||||
return env_token
|
||||
|
||||
# Check cached token freshness
|
||||
now = time.time()
|
||||
if self._cached_token and now < self._token_expiry - 60:
|
||||
return self._cached_token
|
||||
|
||||
# 2. Use google-auth library if available
|
||||
if HAVE_GOOGLE_AUTH:
|
||||
try:
|
||||
credentials, _ = google.auth.default(
|
||||
scopes=["https://www.googleapis.com/auth/cloud-platform"]
|
||||
)
|
||||
auth_req = google.auth.transport.requests.Request()
|
||||
credentials.refresh(auth_req)
|
||||
self._cached_token = credentials.token
|
||||
# Cache for up to 50 minutes
|
||||
self._token_expiry = now + 3000
|
||||
return self._cached_token
|
||||
except Exception as e:
|
||||
# Fall back to gcloud CLI
|
||||
pass
|
||||
|
||||
# 3. Fall back to gcloud CLI
|
||||
if shutil.which("gcloud"):
|
||||
try:
|
||||
token = subprocess.check_output(
|
||||
["gcloud", "auth", "print-access-token"],
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
).strip()
|
||||
if token:
|
||||
self._cached_token = token
|
||||
self._token_expiry = now + 3000
|
||||
return token
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def check_auth(self) -> Tuple[bool, str]:
|
||||
"""Validate whether authentication is ready."""
|
||||
if self.api_key:
|
||||
return True, f"Using direct API key (model: {self.model})"
|
||||
|
||||
if not self.project_id:
|
||||
return False, (
|
||||
"No Google Cloud project ID detected.\n"
|
||||
"Please set VERTEX_PROJECT_ID=<your-project-id> or run:\n"
|
||||
" gcloud config set project <your-project-id>\n"
|
||||
"Refer to botagent_gear/SETUP.md for full instructions."
|
||||
)
|
||||
|
||||
token = self._get_access_token()
|
||||
if not token:
|
||||
return False, (
|
||||
"Unable to obtain Google Cloud authentication token.\n"
|
||||
"Please authenticate using one of the methods in botagent_gear/SETUP.md:\n"
|
||||
" 1. Run: gcloud auth application-default login\n"
|
||||
" 2. Or run: gcloud auth login\n"
|
||||
" 3. Or export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json\n"
|
||||
" 4. Or set GEMINI_API_KEY / VERTEX_API_KEY"
|
||||
)
|
||||
|
||||
return True, f"Authenticated to GCP Project '{self.project_id}' in region '{self.location}' (model: {self.model})"
|
||||
|
||||
def ask_json(self, prompt: str) -> Optional[Dict[str, Any]]:
|
||||
"""Query Gemini requesting structured JSON output."""
|
||||
payload = {
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [{"text": prompt}],
|
||||
}
|
||||
],
|
||||
"systemInstruction": {
|
||||
"parts": [{"text": GAME_RULES_SUMMARY}]
|
||||
},
|
||||
"generationConfig": {
|
||||
"responseMimeType": "application/json",
|
||||
"temperature": 0.3,
|
||||
},
|
||||
}
|
||||
|
||||
# Route to Google AI Studio if API key without GCP Project
|
||||
if self.api_key and not self.project_id:
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:generateContent?key={self.api_key}"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
else:
|
||||
token = self._get_access_token()
|
||||
if not token and not self.api_key:
|
||||
print("⚠️ [VERTEX AUTH ERROR] Not authenticated. See botagent_gear/SETUP.md.")
|
||||
return None
|
||||
|
||||
url = (
|
||||
f"https://{self.location}-aiplatform.googleapis.com/v1/"
|
||||
f"projects/{self.project_id}/locations/{self.location}/"
|
||||
f"publishers/google/models/{self.model}:generateContent"
|
||||
)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif self.api_key:
|
||||
url = f"{url}?key={self.api_key}"
|
||||
|
||||
try:
|
||||
res = requests.post(url, headers=headers, json=payload, timeout=45)
|
||||
if res.status_code == 401:
|
||||
# Token might have expired, invalidate cache and retry once
|
||||
self._cached_token = None
|
||||
new_token = self._get_access_token()
|
||||
if new_token:
|
||||
headers["Authorization"] = f"Bearer {new_token}"
|
||||
res = requests.post(url, headers=headers, json=payload, timeout=45)
|
||||
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
candidates = data.get("candidates", [])
|
||||
if not candidates:
|
||||
print("⚠️ [VERTEX EMPTY CANDIDATES]", data)
|
||||
return None
|
||||
|
||||
parts = candidates[0].get("content", {}).get("parts", [])
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
raw_text = parts[0].get("text", "")
|
||||
return extract_json(raw_text)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"⚠️ [VERTEX HTTP ERROR {res.status_code}]: {res.text}")
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"⚠️ [VERTEX REQUEST ERROR]: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class VertexAIBotAgent:
|
||||
def __init__(
|
||||
self,
|
||||
name: str = DEFAULT_BOT_NAME,
|
||||
color: str = DEFAULT_BOT_COLOR,
|
||||
strength: int = DEFAULT_BOT_STRENGTH,
|
||||
server_url: str = DEFAULT_SERVER_URL,
|
||||
project_id: Optional[str] = None,
|
||||
location: str = DEFAULT_LOCATION,
|
||||
model: str = DEFAULT_MODEL,
|
||||
api_key: Optional[str] = None,
|
||||
):
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.strength = strength
|
||||
self.base_url = normalize_url(server_url)
|
||||
self.llm = VertexGeminiClient(
|
||||
project_id=project_id,
|
||||
location=location,
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
)
|
||||
self.bot_id: Optional[str] = None
|
||||
self.party_id: Optional[str] = None
|
||||
self.is_leader: bool = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Registration / status
|
||||
# ------------------------------------------------------------------
|
||||
def register(self):
|
||||
"""Register the bot avatar on the grid or reconnect if already present."""
|
||||
try:
|
||||
players = requests.get(f"{self.base_url}/players").json()
|
||||
for p in players:
|
||||
if p.get("name") == self.name:
|
||||
self.bot_id = p["id"]
|
||||
print(f"🔄 [RECONNECT] Reconnected to existing {self.name} (ID: {self.bot_id}) at ({p.get('x')}, {p.get('y')})")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
res = requests.post(
|
||||
f"{self.base_url}/players",
|
||||
json={"name": self.name, "color": self.color, "strength": self.strength},
|
||||
)
|
||||
if res.status_code == 400 and "already registered" in res.text:
|
||||
players = requests.get(f"{self.base_url}/players").json()
|
||||
for p in players:
|
||||
if p.get("name") == self.name:
|
||||
self.bot_id = p["id"]
|
||||
print(f"🔄 [RECONNECT] Reconnected to existing {self.name} (ID: {self.bot_id}) at ({p.get('x')}, {p.get('y')})")
|
||||
return
|
||||
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
self.bot_id = data["id"]
|
||||
print(f"🚀 [REGISTER] Spawned {self.name} (ID: {self.bot_id}, Str: {self.strength}) at ({data['x']}, {data['y']})")
|
||||
|
||||
def refresh_status(self):
|
||||
"""Update bot state (party membership, leader status, score)."""
|
||||
res = requests.get(f"{self.base_url}/players/{self.bot_id}")
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
self.party_id = data.get("party_id")
|
||||
self.is_leader = data.get("is_party_leader", False)
|
||||
return data
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Core decision loop
|
||||
# ------------------------------------------------------------------
|
||||
def decide_and_act(self):
|
||||
my_info = self.refresh_status()
|
||||
if not my_info:
|
||||
return
|
||||
|
||||
print(f"\n🤖 --- Turn for {self.name} | Score: {my_info['score']} | Str: {my_info['strength']} | Party: {self.party_id or 'Solo'} ---")
|
||||
|
||||
radar_res = requests.get(f"{self.base_url}/players/{self.bot_id}/radar").json()
|
||||
targets = radar_res.get("targets", [])
|
||||
|
||||
adjacent_target = None
|
||||
for t in targets:
|
||||
if t["distance"] <= 1 and not t["is_ally"]:
|
||||
adjacent_target = t
|
||||
break
|
||||
|
||||
if adjacent_target:
|
||||
self._handle_adjacent_encounter(adjacent_target, my_info)
|
||||
else:
|
||||
self._navigate_towards_goal(radar_res, my_info)
|
||||
|
||||
def _handle_adjacent_encounter(self, target: Dict[str, Any], my_info: Dict[str, Any]):
|
||||
"""Resolve the encounter; Gemini only gets a say when the rules allow a choice."""
|
||||
target_name = target["name"]
|
||||
target_str = target["strength"]
|
||||
target_party = target.get("party_id")
|
||||
|
||||
print(f"🔍 [ADJACENT ENCOUNTER] Next to '{target_name}' (Str: {target_str}, Party: {target_party or 'None'})")
|
||||
|
||||
# SCENARIO A: I am a Solo Bot
|
||||
if not self.party_id:
|
||||
if not target_party:
|
||||
# Both solo: alliance is OPTIONAL - ask Gemini
|
||||
self._decide_voluntary_alliance(target, my_info)
|
||||
else:
|
||||
# Target belongs to a party: joining/refusing is mandated by relative strength
|
||||
party_info = requests.get(f"{self.base_url}/parties/{target_party}").json()
|
||||
target_leader_str = target_str
|
||||
if party_info:
|
||||
leader_player = requests.get(f"{self.base_url}/players/{party_info['leader_id']}").json()
|
||||
target_leader_str = leader_player.get("strength", 1)
|
||||
|
||||
if self.strength <= target_leader_str:
|
||||
print(f"🤝 [RULE] Party leader strength {target_leader_str} >= my {self.strength}. Willingly joining squad!")
|
||||
self._step_or_attack(target)
|
||||
else:
|
||||
print(f"⚔️ [RULE] Party leader is weaker ({target_leader_str} < my {self.strength}). Must refuse and fight!")
|
||||
self._initiate_battle(target["id"])
|
||||
|
||||
# SCENARIO B: I am in a Party
|
||||
else:
|
||||
if not self.is_leader:
|
||||
print("🛡️ [PARTY MEMBER] Under command of party leader. Awaiting leader movement.")
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
return
|
||||
|
||||
if not target_party:
|
||||
if target_str <= self.strength:
|
||||
print(f"🤝 [RULE] Solo bot {target_name} is willing to join our squad under my leadership.")
|
||||
self._step_or_attack(target)
|
||||
else:
|
||||
print(f"⚔️ [RULE] Solo bot {target_name} refuses weaker leader! Squad is attacking!")
|
||||
self._initiate_battle(target["id"])
|
||||
else:
|
||||
print(f"⚔️ [RULE] Hostile party detected: '{target.get('party_name')}'! Battle is mandatory!")
|
||||
self._initiate_battle(target["id"])
|
||||
|
||||
def _decide_voluntary_alliance(self, target: Dict[str, Any], my_info: Dict[str, Any]):
|
||||
"""Ask Gemini whether to propose a voluntary alliance with another solo bot."""
|
||||
prompt = f"""{GAME_RULES_SUMMARY}
|
||||
You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}).
|
||||
You just encountered another solo bot "{target['name']}" (strength {target['strength']}).
|
||||
Whoever has greater strength (or higher score if tied) will lead the new party.
|
||||
Forming an alliance is optional - larger parties are stronger in future battles, but you
|
||||
give up independent control if you are not the stronger one.
|
||||
|
||||
Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reason"}}
|
||||
"""
|
||||
decision = self.llm.ask_json(prompt) or {}
|
||||
form_alliance = decision.get("form_alliance", True)
|
||||
reasoning = decision.get("reasoning", "")
|
||||
|
||||
if form_alliance:
|
||||
leader_id = self.bot_id if self.strength >= target["strength"] else target["id"]
|
||||
print(f"🤝 [GEMINI DECISION] Ally with {target['name']}! Leader: {'me' if leader_id == self.bot_id else target['name']}. {reasoning}")
|
||||
self._execute_party_formation([self.bot_id, target["id"]], leader_id)
|
||||
else:
|
||||
print(f"🚶 [GEMINI DECISION] Declining alliance with {target['name']}. {reasoning}")
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
|
||||
def _execute_party_formation(self, member_ids: List[str], leader_id: str):
|
||||
"""Form a party using the REST API."""
|
||||
try:
|
||||
res = requests.post(
|
||||
f"{self.base_url}/parties",
|
||||
json={"member_ids": member_ids, "leader_id": leader_id, "name": f"Squad_{self.name}"},
|
||||
)
|
||||
if res.status_code == 201:
|
||||
party = res.json()
|
||||
print(f"✅ [PARTY FORMED] Squad '{party['name']}' established! Leader: {party['leader_name']} | Str: {party['total_strength']}")
|
||||
else:
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
except Exception as e:
|
||||
print(f"Party formation error: {e}")
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
|
||||
def _initiate_battle(self, opponent_id: str):
|
||||
"""Explicitly call the 3-Bout D20 Battle endpoint."""
|
||||
print(f"🎲 [BATTLE INITIATED] Clashing with opponent {opponent_id}...")
|
||||
res = requests.post(
|
||||
f"{self.base_url}/battles/fight",
|
||||
json={"challenger_id": self.bot_id, "defender_id": opponent_id},
|
||||
)
|
||||
if res.status_code == 200:
|
||||
battle = res.json()
|
||||
print("\n--- ⚔️ 3-BOUT BATTLE RESOLUTION ---")
|
||||
print(f"Bouts won: {battle['party1_name']} ({battle['party1_bouts_won']}) vs {battle['party2_name']} ({battle['party2_bouts_won']})")
|
||||
for b in battle["bouts"]:
|
||||
print(f" Bout #{b['bout_number']}: Roll {b['party1_roll']}×{b['party1_strength']} ({b['party1_score']}) vs Roll {b['party2_roll']}×{b['party2_strength']} ({b['party2_score']}) -> Winner: {b['winner_name']}")
|
||||
print(f"🏆 Overall Winner: {battle['winner_party_name']} (Leader {battle['winner_leader_name']} receives +2 pts)")
|
||||
print(f"💀 Defeated: {battle['defeated_party_name']} (Leader {battle['killed_leader_name']} -1 pt)")
|
||||
if battle.get("absorbed_members"):
|
||||
print(f"🧲 Absorbed {len(battle['absorbed_members'])} member(s) into {battle['winner_party_name']}")
|
||||
else:
|
||||
print(f"Battle failed ({res.status_code}): {res.text}")
|
||||
|
||||
def _step_or_attack(self, target: Dict[str, Any]):
|
||||
"""Move adjacent/towards the target while avoiding obstacles."""
|
||||
moves_res = requests.get(f"{self.base_url}/players/{self.bot_id}/available-moves").json()
|
||||
moves = moves_res.get("moves", {})
|
||||
chosen = self._closest_direction_to(target["x"], target["y"], moves)
|
||||
|
||||
if chosen:
|
||||
res = requests.post(f"{self.base_url}/players/{self.bot_id}/move", json={"direction": chosen}).json()
|
||||
if res.get("battle_triggered"):
|
||||
print(f"⚔️ Move triggered battle! Winner: {res['battle_result']['winner_party_name']}")
|
||||
elif res.get("party_formed_triggered"):
|
||||
print("🤝 Move resulted in party alliance!")
|
||||
else:
|
||||
print("⚠️ No passable moves adjacent to target. Passing turn.")
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
|
||||
@staticmethod
|
||||
def _closest_direction_to(target_x: int, target_y: int, moves: Dict[str, Any]) -> Optional[str]:
|
||||
"""Pick the available direction that minimizes Chebyshev distance to (target_x, target_y)."""
|
||||
valid_moves = {d: chk for d, chk in moves.items() if chk.get("available")}
|
||||
if not valid_moves:
|
||||
return None
|
||||
|
||||
def dist(chk: Dict[str, Any]) -> int:
|
||||
return max(abs(chk["target_x"] - target_x), abs(chk["target_y"] - target_y))
|
||||
|
||||
return min(valid_moves.keys(), key=lambda d: (valid_moves[d].get("strength_penalty", 0.0) > 0, dist(valid_moves[d])))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Navigation (Gemini-driven direction choice among legal moves)
|
||||
# ------------------------------------------------------------------
|
||||
def _navigate_towards_goal(self, radar_res: Dict[str, Any], my_info: Dict[str, Any]):
|
||||
moves_res = requests.get(f"{self.base_url}/players/{self.bot_id}/available-moves").json()
|
||||
moves = moves_res.get("moves", {})
|
||||
available = [d for d, chk in moves.items() if chk.get("available")]
|
||||
|
||||
if not available:
|
||||
print("🚫 All adjacent paths blocked by borders/obstacles. Passing turn.")
|
||||
requests.post(f"{self.base_url}/players/{self.bot_id}/pass")
|
||||
return
|
||||
|
||||
chosen_dir = self._ask_llm_for_direction(radar_res, moves, available, my_info)
|
||||
if chosen_dir not in available:
|
||||
# Guard-rail: fall back to server-recommended or nearest-distance direction
|
||||
rec_dir = radar_res.get("recommended_direction")
|
||||
nearest = radar_res.get("nearest_target")
|
||||
if rec_dir in available:
|
||||
chosen_dir = rec_dir
|
||||
elif nearest:
|
||||
chosen_dir = self._closest_direction_to(nearest["x"], nearest["y"], moves) or available[0]
|
||||
else:
|
||||
chosen_dir = available[0]
|
||||
|
||||
print(f"🧭 Moving {chosen_dir} (Goal: {radar_res.get('bot_goal')}, Action: {radar_res.get('recommended_action')})")
|
||||
res = requests.post(f"{self.base_url}/players/{self.bot_id}/move", json={"direction": chosen_dir}).json()
|
||||
|
||||
if res.get("battle_triggered"):
|
||||
print(f"⚔️ Encounter battle! Winner: {res['battle_result']['winner_party_name']}")
|
||||
elif res.get("party_formed_triggered"):
|
||||
print(f"🤝 Formed or joined squad: {res.get('formed_party', {}).get('name')}")
|
||||
|
||||
def _get_board_snapshot(self) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch the full board so Gemini sees more than radar's nearest few."""
|
||||
try:
|
||||
res = requests.get(f"{self.base_url}/board", timeout=10)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"⚠️ [BOARD FETCH ERROR] {e}")
|
||||
return None
|
||||
|
||||
OBSTACLE_SYMBOLS = {"mountain": "M", "forest": "F", "valley": "V"}
|
||||
LOCAL_MAP_RADIUS = 8
|
||||
|
||||
def _build_local_map(self, board: Dict[str, Any], center_x: int, center_y: int) -> List[str]:
|
||||
"""Render an ASCII minimap centered on the bot: @ = self, A = ally, E = enemy, M/F/V = obstacles, . = open."""
|
||||
config = board.get("config", {})
|
||||
max_x = config.get("max_x", 64)
|
||||
max_y = config.get("max_y", 64)
|
||||
radius = self.LOCAL_MAP_RADIUS
|
||||
|
||||
obstacle_at = {(o["x"], o["y"]): o.get("type", "mountain") for o in board.get("obstacles", [])}
|
||||
player_at: Dict[Tuple[int, int], List[Dict[str, Any]]] = {}
|
||||
for p in board.get("players", []):
|
||||
if p["id"] == self.bot_id:
|
||||
continue
|
||||
player_at.setdefault((p["x"], p["y"]), []).append(p)
|
||||
|
||||
rows = []
|
||||
for y in range(center_y - radius, center_y + radius + 1):
|
||||
row_chars = []
|
||||
for x in range(center_x - radius, center_x + radius + 1):
|
||||
if x == center_x and y == center_y:
|
||||
row_chars.append("@")
|
||||
elif x < 0 or y < 0 or x >= max_x or y >= max_y:
|
||||
row_chars.append("#")
|
||||
elif (x, y) in obstacle_at:
|
||||
row_chars.append(self.OBSTACLE_SYMBOLS.get(obstacle_at[(x, y)], "M"))
|
||||
elif (x, y) in player_at:
|
||||
occupants = player_at[(x, y)]
|
||||
is_ally = self.party_id and any(o.get("party_id") == self.party_id for o in occupants)
|
||||
row_chars.append("A" if is_ally else "E")
|
||||
else:
|
||||
row_chars.append(".")
|
||||
rows.append("".join(row_chars))
|
||||
return rows
|
||||
|
||||
def _ask_llm_for_direction(
|
||||
self,
|
||||
radar_res: Dict[str, Any],
|
||||
moves: Dict[str, Any],
|
||||
available: List[str],
|
||||
my_info: Dict[str, Any],
|
||||
) -> Optional[str]:
|
||||
targets_summary = [
|
||||
{
|
||||
"name": t["name"],
|
||||
"x": t["x"],
|
||||
"y": t["y"],
|
||||
"distance": t["distance"],
|
||||
"strength": t["strength"],
|
||||
"party": t.get("party_name") or ("solo" if not t.get("party_id") else t.get("party_id")),
|
||||
"is_ally": t.get("is_ally", False),
|
||||
}
|
||||
for t in radar_res.get("targets", [])
|
||||
]
|
||||
moves_summary = {
|
||||
d: {
|
||||
"target_x": chk.get("target_x"),
|
||||
"target_y": chk.get("target_y"),
|
||||
"strength_penalty": chk.get("strength_penalty", 0.0),
|
||||
}
|
||||
for d, chk in moves.items()
|
||||
if d in available
|
||||
}
|
||||
|
||||
board = self._get_board_snapshot()
|
||||
map_section = ""
|
||||
if board:
|
||||
local_map = self._build_local_map(board, my_info["x"], my_info["y"])
|
||||
map_section = f"""
|
||||
Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is
|
||||
increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot,
|
||||
M = mountain, F = forest, V = valley, # = out of bounds, . = open ground.
|
||||
{chr(10).join(local_map)}
|
||||
"""
|
||||
|
||||
prompt = f"""{GAME_RULES_SUMMARY}
|
||||
You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, party: {self.party_id or 'Solo'})
|
||||
at position ({my_info['x']}, {my_info['y']}).
|
||||
Server's radar suggestion: recommended_direction={radar_res.get('recommended_direction')},
|
||||
recommended_action={radar_res.get('recommended_action')}, goal={radar_res.get('bot_goal')}.
|
||||
{map_section}
|
||||
All known bots/parties on the board (sorted nearest first): {json.dumps(targets_summary)}
|
||||
Your ONLY legal moves this turn, with resulting coordinates and any strength penalty for
|
||||
squeezing past obstacles: {json.dumps(moves_summary)}
|
||||
|
||||
Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow
|
||||
your party, avoid stronger hostile parties, route around obstacles visible on the map, minimize
|
||||
strength penalties, or explore if nothing is nearby). You MUST pick a key from the legal moves
|
||||
object above.
|
||||
|
||||
Respond ONLY with JSON: {{"direction": "<one of {available}>", "reasoning": "short reason"}}
|
||||
"""
|
||||
decision = self.llm.ask_json(prompt) or {}
|
||||
direction = decision.get("direction")
|
||||
reasoning = decision.get("reasoning", "")
|
||||
if reasoning:
|
||||
print(f"🧠 [GEMINI] {reasoning}")
|
||||
return direction
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ------------------------------------------------------------------
|
||||
def run(self):
|
||||
# Validate authentication upfront
|
||||
auth_ok, auth_msg = self.llm.check_auth()
|
||||
if not auth_ok:
|
||||
print(f"\n❌ [AUTH SETUP REQUIRED]\n{auth_msg}\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"✨ [AUTH SUCCESS] {auth_msg}")
|
||||
|
||||
self.register()
|
||||
try:
|
||||
while True:
|
||||
turn_info = requests.get(f"{self.base_url}/turn").json()
|
||||
if not turn_info.get("game_started", False):
|
||||
if self.bot_id:
|
||||
res = requests.get(f"{self.base_url}/players/{self.bot_id}")
|
||||
if res.status_code == 404:
|
||||
print("\n⚠️ [RESET] Board was regenerated. Rejoining lobby...")
|
||||
self.register()
|
||||
|
||||
print("⏳ [LOBBY] Waiting for game to start via 'Start Game' in UI... ", end="\r", flush=True)
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
curr_player_id = turn_info.get("current_player_id")
|
||||
|
||||
if curr_player_id == self.bot_id:
|
||||
self.decide_and_act()
|
||||
|
||||
conc = requests.get(f"{self.base_url}/game/conclusion").json()
|
||||
if conc.get("concluded"):
|
||||
print(f"\n🎉 [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!")
|
||||
break
|
||||
else:
|
||||
time.sleep(0.4)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\nDisconnecting {self.name}...")
|
||||
if self.bot_id:
|
||||
requests.delete(f"{self.base_url}/players/{self.bot_id}")
|
||||
|
||||
|
||||
def main():
|
||||
env_url = os.environ.get("BOT_SERVER_URL") or os.environ.get("SERVER_URL") or DEFAULT_SERVER_URL
|
||||
env_name = os.environ.get("BOT_NAME", DEFAULT_BOT_NAME)
|
||||
env_color = os.environ.get("BOT_COLOR", DEFAULT_BOT_COLOR)
|
||||
env_strength = int(os.environ.get("BOT_STRENGTH", str(DEFAULT_BOT_STRENGTH)))
|
||||
env_project = os.environ.get("VERTEX_PROJECT_ID") or os.environ.get("GCP_PROJECT") or os.environ.get("GOOGLE_CLOUD_PROJECT")
|
||||
env_location = os.environ.get("VERTEX_LOCATION") or os.environ.get("GCP_REGION") or DEFAULT_LOCATION
|
||||
env_model = os.environ.get("VERTEX_MODEL", DEFAULT_MODEL)
|
||||
env_api_key = os.environ.get("VERTEX_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Vertex AI (Gemini) Bot Agent for botWebWars",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-u", "--url", dest="server_url", default=env_url,
|
||||
help="Backend REST API base URL (env: BOT_SERVER_URL)")
|
||||
parser.add_argument("-n", "--name", dest="name", default=env_name,
|
||||
help="Display name for this bot (env: BOT_NAME)")
|
||||
parser.add_argument("-c", "--color", dest="color", default=env_color,
|
||||
help="Hex color code for the bot avatar (env: BOT_COLOR)")
|
||||
parser.add_argument("-s", "--strength", dest="strength", type=int, default=env_strength,
|
||||
help="Strength attribute (1-10) for D20 battle multiplier (env: BOT_STRENGTH)")
|
||||
parser.add_argument("-p", "--project", dest="project_id", default=env_project,
|
||||
help="Google Cloud Project ID (env: VERTEX_PROJECT_ID or GCP_PROJECT)")
|
||||
parser.add_argument("-l", "--location", dest="location", default=env_location,
|
||||
help="Vertex AI region / location (env: VERTEX_LOCATION)")
|
||||
parser.add_argument("-m", "--model", dest="model", default=env_model,
|
||||
help="Gemini model ID (env: VERTEX_MODEL)")
|
||||
parser.add_argument("-k", "--api-key", dest="api_key", default=env_api_key,
|
||||
help="Gemini API Key or Vertex AI express mode key (env: VERTEX_API_KEY or GEMINI_API_KEY)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"--- Vertex AI Gemini Bot Agent ---")
|
||||
print(f"Target Model: {args.model}")
|
||||
print(f"Region: {args.location}")
|
||||
if args.project_id:
|
||||
print(f"GCP Project: {args.project_id}")
|
||||
print()
|
||||
|
||||
agent = VertexAIBotAgent(
|
||||
name=args.name,
|
||||
color=args.color,
|
||||
strength=args.strength,
|
||||
server_url=args.server_url,
|
||||
project_id=args.project_id,
|
||||
location=args.location,
|
||||
model=args.model,
|
||||
api_key=args.api_key,
|
||||
)
|
||||
agent.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
requests>=2.31.0
|
||||
google-auth>=2.29.0
|
||||
Loading…
Reference in New Issue