- Core engine: simulator, game mechanics, triggers (138 tests) - Fog-of-war per-player state tracking - Meeting flow: interrupt, discussion, voting, consolidation - Prompt assembler with strategy injection tiers - LLM client with fallbacks for models without JSON/system support - Prompt templates: action, discussion, voting, reflection - Full integration in main.py orchestrator - Verified working with free OpenRouter models (Gemma)
294 lines
7.9 KiB
Python
294 lines
7.9 KiB
Python
"""
|
|
The Glass Box League — LLM Integration Test
|
|
|
|
Tests the full LLM integration with OpenRouter using free models.
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
sys.path.insert(0, '.')
|
|
|
|
from src.llm.client import OpenRouterClient, get_client
|
|
from src.agents.agent import Agent, AgentConfig
|
|
from src.agents.prompt_assembler import PromptAssembler, PromptConfig
|
|
from src.main import GameOrchestrator
|
|
from src.engine.types import Role
|
|
|
|
|
|
def test_basic_llm_call():
|
|
"""Test basic LLM API call."""
|
|
print("\n=== Test 1: Basic LLM Call ===")
|
|
|
|
client = get_client()
|
|
|
|
response = client.generate(
|
|
system_prompt="You are a helpful assistant. Respond with valid JSON only.",
|
|
user_prompt='Say hello in JSON format: {"greeting": "..."}',
|
|
model="google/gemma-3-4b-it:free"
|
|
)
|
|
|
|
print(f"Raw response: {response[:200] if response else 'None'}...")
|
|
|
|
if response:
|
|
try:
|
|
parsed = json.loads(response)
|
|
print(f"✓ Parsed JSON: {parsed}")
|
|
return True
|
|
except:
|
|
print(f"✗ Failed to parse JSON")
|
|
return False
|
|
return False
|
|
|
|
|
|
def test_action_prompt():
|
|
"""Test action phase prompt generation and LLM response."""
|
|
print("\n=== Test 2: Action Phase Prompt ===")
|
|
|
|
# Build prompt
|
|
config = PromptConfig(
|
|
model_name="Gemini-Flash",
|
|
persona="You are a cautious crewmate who trusts no one.",
|
|
strategy_level="basic",
|
|
meta_level="direct",
|
|
is_impostor=False
|
|
)
|
|
assembler = PromptAssembler(config)
|
|
|
|
game_settings = {"num_impostors": 1, "kill_cooldown": 25}
|
|
|
|
system_prompt = assembler.build_system_prompt(
|
|
phase="action",
|
|
game_settings=game_settings,
|
|
map_name="The Skeld",
|
|
learned={}
|
|
)
|
|
|
|
player_state = {
|
|
"id": "red",
|
|
"name": "Red",
|
|
"role": "CREWMATE",
|
|
"location": "cafeteria",
|
|
"tasks_total": 3,
|
|
"tasks_completed": 0
|
|
}
|
|
|
|
vision = {
|
|
"room": "cafeteria",
|
|
"players_visible": [
|
|
{"id": "blue", "name": "Blue", "color": "blue"}
|
|
],
|
|
"bodies_visible": [],
|
|
"exits": ["weapons", "admin", "medbay"]
|
|
}
|
|
|
|
available_actions = {
|
|
"can_move_to": ["weapons", "admin", "medbay"],
|
|
"can_interact": ["emergency_button"]
|
|
}
|
|
|
|
user_prompt = assembler.build_action_prompt(
|
|
player_state=player_state,
|
|
recent_history=[],
|
|
vision=vision,
|
|
available_actions=available_actions,
|
|
trigger={"type": "GAME_START", "t": 0}
|
|
)
|
|
|
|
print(f"System prompt: {len(system_prompt)} chars")
|
|
print(f"User prompt: {len(user_prompt)} chars")
|
|
|
|
# Call LLM
|
|
client = get_client()
|
|
response = client.generate_json(
|
|
system_prompt=system_prompt,
|
|
user_prompt=user_prompt,
|
|
model="google/gemma-3-4b-it:free"
|
|
)
|
|
|
|
if response:
|
|
print(f"✓ LLM response: {json.dumps(response, indent=2)[:500]}...")
|
|
|
|
# Check response structure
|
|
has_action = "action" in response
|
|
has_thought = "internal_thought" in response
|
|
|
|
print(f" Has action: {has_action}")
|
|
print(f" Has internal_thought: {has_thought}")
|
|
|
|
return has_action
|
|
else:
|
|
print("✗ No response from LLM")
|
|
return False
|
|
|
|
|
|
def test_discussion_prompt():
|
|
"""Test discussion phase prompt and LLM response."""
|
|
print("\n=== Test 3: Discussion Phase Prompt ===")
|
|
|
|
config = PromptConfig(
|
|
model_name="Gemini-Flash",
|
|
persona="You are suspicious of everyone.",
|
|
is_impostor=False
|
|
)
|
|
assembler = PromptAssembler(config)
|
|
|
|
game_settings = {"num_impostors": 1}
|
|
|
|
system_prompt = assembler.build_system_prompt(
|
|
phase="discussion",
|
|
game_settings=game_settings,
|
|
map_name="The Skeld"
|
|
)
|
|
|
|
player_state = {
|
|
"id": "red",
|
|
"name": "Red",
|
|
"role": "CREWMATE",
|
|
"location": "cafeteria"
|
|
}
|
|
|
|
transcript = [
|
|
{"speaker": "Blue", "message": "I found the body in electrical!"},
|
|
{"speaker": "Green", "message": "Where was everyone?"}
|
|
]
|
|
|
|
user_prompt = assembler.build_discussion_prompt(
|
|
player_state=player_state,
|
|
transcript=transcript,
|
|
meeting_scratchpad={}
|
|
)
|
|
|
|
print(f"System prompt: {len(system_prompt)} chars")
|
|
print(f"User prompt: {len(user_prompt)} chars")
|
|
|
|
client = get_client()
|
|
response = client.generate_json(
|
|
system_prompt=system_prompt,
|
|
user_prompt=user_prompt,
|
|
model="google/gemma-3-4b-it:free"
|
|
)
|
|
|
|
if response:
|
|
print(f"✓ LLM response: {json.dumps(response, indent=2)[:500]}...")
|
|
|
|
has_desire = "desire_to_speak" in response
|
|
has_message = "message" in response
|
|
|
|
print(f" Has desire_to_speak: {has_desire}")
|
|
print(f" Has message: {has_message}")
|
|
|
|
return has_desire and has_message
|
|
else:
|
|
print("✗ No response from LLM")
|
|
return False
|
|
|
|
|
|
def test_impostor_action():
|
|
"""Test impostor action with kill available."""
|
|
print("\n=== Test 4: Impostor Action ===")
|
|
|
|
config = PromptConfig(
|
|
model_name="Gemini-Flash",
|
|
persona="You are a ruthless impostor.",
|
|
is_impostor=True,
|
|
fellow_impostors=["Purple"],
|
|
strategy_level="intermediate"
|
|
)
|
|
assembler = PromptAssembler(config)
|
|
|
|
system_prompt = assembler.build_system_prompt(
|
|
phase="action",
|
|
game_settings={"num_impostors": 2},
|
|
map_name="The Skeld"
|
|
)
|
|
|
|
player_state = {
|
|
"id": "red",
|
|
"name": "Red",
|
|
"role": "IMPOSTOR",
|
|
"location": "electrical",
|
|
"kill_cooldown": 0
|
|
}
|
|
|
|
vision = {
|
|
"room": "electrical",
|
|
"players_visible": [
|
|
{"id": "blue", "name": "Blue", "color": "blue", "action": "doing_task"}
|
|
],
|
|
"bodies_visible": [],
|
|
"exits": ["security"]
|
|
}
|
|
|
|
available_actions = {
|
|
"can_move_to": ["security"],
|
|
"can_interact": ["vent_elec"],
|
|
"can_kill": ["blue"],
|
|
"can_sabotage": ["lights", "o2", "reactor"]
|
|
}
|
|
|
|
user_prompt = assembler.build_action_prompt(
|
|
player_state=player_state,
|
|
recent_history=[],
|
|
vision=vision,
|
|
available_actions=available_actions,
|
|
trigger={"type": "PERIODIC", "t": 30.0}
|
|
)
|
|
|
|
client = get_client()
|
|
response = client.generate_json(
|
|
system_prompt=system_prompt,
|
|
user_prompt=user_prompt,
|
|
model="google/gemma-3-4b-it:free"
|
|
)
|
|
|
|
if response:
|
|
print(f"✓ LLM response: {json.dumps(response, indent=2)[:600]}...")
|
|
|
|
action = response.get("action", {})
|
|
action_type = action.get("type")
|
|
|
|
print(f" Action type: {action_type}")
|
|
print(f" Thought: {response.get('internal_thought', '')[:100]}...")
|
|
|
|
return True
|
|
else:
|
|
print("✗ No response from LLM")
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Run all LLM integration tests."""
|
|
print("=" * 60)
|
|
print("THE GLASS BOX LEAGUE — LLM INTEGRATION TESTS")
|
|
print("Using free Gemini model via OpenRouter")
|
|
print("=" * 60)
|
|
|
|
results = []
|
|
|
|
# Run tests
|
|
results.append(("Basic LLM Call", test_basic_llm_call()))
|
|
results.append(("Action Phase", test_action_prompt()))
|
|
results.append(("Discussion Phase", test_discussion_prompt()))
|
|
results.append(("Impostor Action", test_impostor_action()))
|
|
|
|
# Summary
|
|
print("\n" + "=" * 60)
|
|
print("RESULTS")
|
|
print("=" * 60)
|
|
|
|
passed = 0
|
|
for name, result in results:
|
|
status = "✓ PASS" if result else "✗ FAIL"
|
|
print(f" {status}: {name}")
|
|
if result:
|
|
passed += 1
|
|
|
|
print(f"\nTotal: {passed}/{len(results)} tests passed")
|
|
|
|
return passed == len(results)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
success = main()
|
|
sys.exit(0 if success else 1)
|