AI_Diplomacy/ai_diplomacy/prompt_constructor.py

226 lines
No EOL
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Module for constructing prompts for LLM interactions in the Diplomacy game.
"""
import logging
from typing import Dict, List, Optional, Any # Added Any for game type placeholder
from .utils import load_prompt
from .possible_order_context import (
generate_rich_order_context,
generate_rich_order_context_xml,
)
import os
from .game_history import GameHistory # Assuming GameHistory is correctly importable
# placeholder for diplomacy.Game to avoid circular or direct dependency if not needed for typehinting only
# from diplomacy import Game # Uncomment if 'Game' type hint is crucial and available
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Or inherit from parent logger
# --- Home-center lookup -------------------------------------------
HOME_CENTERS: dict[str, list[str]] = {
"AUSTRIA": ["Budapest", "Trieste", "Vienna"],
"ENGLAND": ["Edinburgh", "Liverpool", "London"],
"FRANCE": ["Brest", "Marseilles", "Paris"],
"GERMANY": ["Berlin", "Kiel", "Munich"],
"ITALY": ["Naples", "Rome", "Venice"],
"RUSSIA": ["Moscow", "Saint Petersburg", "Sevastopol", "Warsaw"],
"TURKEY": ["Ankara", "Constantinople", "Smyrna"],
}
def build_context_prompt(
game: Any, # diplomacy.Game object
board_state: dict,
power_name: str,
possible_orders: Dict[str, List[str]],
game_history: GameHistory,
agent_goals: Optional[List[str]] = None,
agent_relationships: Optional[Dict[str, str]] = None,
agent_private_diary: Optional[str] = None,
prompts_dir: Optional[str] = None,
include_messages: Optional[bool] = True,
) -> str:
"""Builds the detailed context part of the prompt.
Args:
game: The game object.
board_state: Current state of the board.
power_name: The name of the power for whom the context is being built.
possible_orders: Dictionary of possible orders.
game_history: History of the game (messages, etc.).
agent_goals: Optional list of agent's goals.
agent_relationships: Optional dictionary of agent's relationships with other powers.
agent_private_diary: Optional string of agent's private diary.
prompts_dir: Optional path to the prompts directory.
Returns:
A string containing the formatted context.
"""
context_template = load_prompt("context_prompt.txt", prompts_dir=prompts_dir)
# === Agent State Debug Logging ===
if agent_goals:
logger.debug(f"Using goals for {power_name}: {agent_goals}")
if agent_relationships:
logger.debug(f"Using relationships for {power_name}: {agent_relationships}")
if agent_private_diary:
logger.debug(f"Using private diary for {power_name}: {agent_private_diary[:200]}...")
# ================================
# Get our units and centers (not directly used in template, but good for context understanding)
# units_info = board_state["units"].get(power_name, [])
# centers_info = board_state["centers"].get(power_name, [])
# Get the current phase
year_phase = board_state["phase"] # e.g. 'S1901M'
# Decide which context builder to use.
_use_simple = os.getenv("SIMPLE_PROMPTS", "0").lower() in {"1", "true", "yes"}
if _use_simple:
possible_orders_context_str = generate_rich_order_context(
game, power_name, possible_orders
)
else:
possible_orders_context_str = generate_rich_order_context_xml(
game, power_name, possible_orders
)
if include_messages:
messages_this_round_text = game_history.get_messages_this_round(
power_name=power_name,
current_phase_name=year_phase
)
if not messages_this_round_text.strip():
messages_this_round_text = "\n(No messages this round)\n"
else:
messages_this_round_text = "\n"
# Separate active and eliminated powers for clarity
active_powers = [p for p in game.powers.keys() if not game.powers[p].is_eliminated()]
eliminated_powers = [p for p in game.powers.keys() if game.powers[p].is_eliminated()]
# Build units representation with power status
units_lines = []
for p, u in board_state["units"].items():
u_str = ", ".join(u)
if game.powers[p].is_eliminated():
units_lines.append(f" {p}: {u_str} [ELIMINATED]")
else:
units_lines.append(f" {p}: {u_str}")
units_repr = "\n".join(units_lines)
# Build centers representation with power status
centers_lines = []
for p, c in board_state["centers"].items():
c_str = ", ".join(c)
if game.powers[p].is_eliminated():
centers_lines.append(f" {p}: {c_str} [ELIMINATED]")
else:
centers_lines.append(f" {p}: {c_str}")
centers_repr = "\n".join(centers_lines)
# Build {home_centers}
home_centers_str = ", ".join(HOME_CENTERS.get(power_name.upper(), []))
order_history_str = game_history.get_order_history_for_prompt(
game=game, # Pass the game object for normalization
power_name=power_name,
current_phase_name=year_phase,
num_movement_phases_to_show=1
)
# Replace token only if it exists (template may not include it)
if "{home_centers}" in context_template:
context_template = context_template.replace("{home_centers}", home_centers_str)
# Following the pattern for home_centers, use replace for safety
if "{order_history}" in context_template:
context_template = context_template.replace("{order_history}", order_history_str)
context = context_template.format(
power_name=power_name,
current_phase=year_phase,
all_unit_locations=units_repr,
all_supply_centers=centers_repr,
messages_this_round=messages_this_round_text,
possible_orders=possible_orders_context_str,
agent_goals="\n".join(f"- {g}" for g in agent_goals) if agent_goals else "None specified",
agent_relationships="\n".join(f"- {p}: {s}" for p, s in agent_relationships.items()) if agent_relationships else "None specified",
agent_private_diary=agent_private_diary if agent_private_diary else "(No diary entries yet)",
)
return context
def construct_order_generation_prompt(
system_prompt: str,
game: Any, # diplomacy.Game object
board_state: dict,
power_name: str,
possible_orders: Dict[str, List[str]],
game_history: GameHistory,
agent_goals: Optional[List[str]] = None,
agent_relationships: Optional[Dict[str, str]] = None,
agent_private_diary_str: Optional[str] = None,
prompts_dir: Optional[str] = None,
) -> str:
"""Constructs the final prompt for order generation.
Args:
system_prompt: The base system prompt for the LLM.
game: The game object.
board_state: Current state of the board.
power_name: The name of the power for whom the prompt is being built.
possible_orders: Dictionary of possible orders.
game_history: History of the game (messages, etc.).
agent_goals: Optional list of agent's goals.
agent_relationships: Optional dictionary of agent's relationships with other powers.
agent_private_diary_str: Optional string of agent's private diary.
prompts_dir: Optional path to the prompts directory.
Returns:
A string containing the complete prompt for the LLM.
"""
# Load prompts
_ = load_prompt("few_shot_example.txt", prompts_dir=prompts_dir) # Loaded but not used, as per original logic
# Pick the phase-specific instruction file (using unformatted versions)
phase_code = board_state["phase"][-1] # 'M' (movement), 'R', or 'A' / 'B'
if phase_code == "M":
instructions_file = "unformatted/order_instructions_movement_phase.txt"
elif phase_code in ("A", "B"): # builds / adjustments
instructions_file = "unformatted/order_instructions_adjustment_phase.txt"
elif phase_code == "R": # retreats
instructions_file = "unformatted/order_instructions_retreat_phase.txt"
else: # unexpected default to movement rules
instructions_file = "unformatted/order_instructions_movement_phase.txt"
instructions = load_prompt(instructions_file, prompts_dir=prompts_dir)
_use_simple = os.getenv("SIMPLE_PROMPTS", "0").lower() in {"1", "true", "yes"}
# Build the context prompt
context = build_context_prompt(
game,
board_state,
power_name,
possible_orders,
game_history,
agent_goals=agent_goals,
agent_relationships=agent_relationships,
agent_private_diary=agent_private_diary_str,
prompts_dir=prompts_dir,
include_messages=not _use_simple, # include only when *not* simple
)
# Append goals at the end for focus
goals_section = ""
if agent_goals:
goals_section = "\n\nYOUR STRATEGIC GOALS:\n" + "\n".join(f"- {g}" for g in agent_goals) + "\n\nKeep these goals in mind when choosing your orders."
final_prompt = system_prompt + "\n\n" + context + "\n\n" + instructions + goals_section
# Make the power names more LLM friendly
final_prompt = final_prompt.replace('AUSTRIA', 'Austria').replace('ENGLAND', "England").replace('FRANCE', 'France').replace('GERMANY', 'Germany').replace('ITALY', "Italy").replace('RUSSIA', 'Russia').replace('TURKEY', 'Turkey')
logger.debug(f"Final order generation prompt preview for {power_name}: {final_prompt[:500]}...")
return final_prompt