fixed prompts, improved negotiations, and diaries

This commit is contained in:
AlxAI 2025-05-17 20:00:14 -04:00
parent 8c87d6f050
commit 7fe6544667
7 changed files with 258 additions and 83 deletions

View file

@ -99,6 +99,14 @@ class DiplomacyAgent:
def _extract_json_from_text(self, text: str) -> dict:
"""Extract and parse JSON from text, handling common LLM response formats."""
# Preprocessing: Normalize common formatting issues
# This helps with the KeyError: '\n "negotiation_summary"' problem
text = re.sub(r'\n\s+"(\w+)"\s*:', r'"\1":', text) # Remove newlines before keys
# Also fix the specific pattern that's causing trouble
text = text.replace('\n "negotiation_summary"', '"negotiation_summary"')
text = text.replace('\n "relationship_updates"', '"relationship_updates"')
text = text.replace('\n "updated_relationships"', '"updated_relationships"')
# Try different patterns to extract JSON
# 1. Try to find JSON wrapped in markdown code blocks
patterns = [
@ -115,17 +123,35 @@ class DiplomacyAgent:
# Try each match until one parses successfully
for match in matches:
try:
return json.loads(match) # First attempt with the raw match
# Additional preprocessing for common formatting issues
clean_match = re.sub(r'\n\s+"(\w+)"\s*:', r'"\1":', match) # Remove newlines before JSON keys
clean_match = re.sub(r',\s*}', '}', clean_match) # Remove trailing commas
return json.loads(clean_match) # First attempt with the cleaned match
except json.JSONDecodeError as e_initial_markdown_parse:
# If initial parsing of the markdown-extracted block fails, try surgical cleaning
try:
# Regex to find and remove sentence-like text ending with a period,
# when it appears before a comma, closing brace/bracket, or at the end of the object.
# Targets interjections like "Phosphorous acid." or "Inhaled."
# Apply several different cleaning patterns to fix common LLM-generated JSON issues
cleaned_match_candidate = match
# Pattern 1: Removes 'Sentence.' when followed by ',', '}', or ']'
cleaned_match_candidate = re.sub(r'\s*([A-Z][\w\s,]*?\.(?:\s+[A-Z][\w\s,]*?\.)*)\s*(?=[,\}\]])', '', match)
cleaned_match_candidate = re.sub(r'\s*([A-Z][\w\s,]*?\.(?:\s+[A-Z][\w\s,]*?\.)*)\s*(?=[,\}\]])', '', cleaned_match_candidate)
# Pattern 2: Removes 'Sentence.' when it's at the very end, before the final '}' of the current match scope
cleaned_match_candidate = re.sub(r'\s*([A-Z][\w\s,]*?\.(?:\s+[A-Z][\w\s,]*?\.)*)\s*(?=\s*\}\s*$)', '', cleaned_match_candidate)
# Pattern 3: Fix for newlines and spaces before JSON keys (common problem with LLMs)
cleaned_match_candidate = re.sub(r'\n\s+"(\w+)"\s*:', r'"\1":', cleaned_match_candidate)
# Pattern 4: Fix trailing commas in JSON objects
cleaned_match_candidate = re.sub(r',\s*}', '}', cleaned_match_candidate)
# Pattern 5: Handle specific known problematic patterns
cleaned_match_candidate = cleaned_match_candidate.replace('\n "negotiation_summary"', '"negotiation_summary"')
cleaned_match_candidate = cleaned_match_candidate.replace('\n "relationship_updates"', '"relationship_updates"')
cleaned_match_candidate = cleaned_match_candidate.replace('\n "updated_relationships"', '"updated_relationships"')
# Pattern 6: Fix quotes - replace single quotes with double quotes for keys
cleaned_match_candidate = re.sub(r"'(\w+)'\s*:", r'"\1":', cleaned_match_candidate)
if cleaned_match_candidate != match: # Log if actual cleaning happened
logger.debug(f"Surgically cleaned JSON candidate. Original snippet: '{match[:150]}...', Cleaned snippet: '{cleaned_match_candidate[:150]}...'")
@ -237,15 +263,12 @@ class DiplomacyAgent:
success_status = "Failure: Initialized" # Default
try:
# Load the template file but safely preprocess it first
prompt_template_content = _load_prompt_file('negotiation_diary_prompt.txt')
if not prompt_template_content:
logger.error(f"[{self.power_name}] Could not load negotiation_diary_prompt.txt. Skipping diary entry.")
success_status = "Failure: Prompt file not loaded"
# No LLM call, so log_llm_response won't have typical LLM data, but we still log the attempt.
# Or, decide not to log if no LLM call is even attempted. For consistency, let's log an attempt.
# To do that, we'd need to call log_llm_response here or ensure finally block handles it.
# For now, the finally block will catch this, but raw_response and full_prompt will be empty.
return # Exit early if prompt is critical
return # Exit early if prompt can't be loaded
# Prepare context for the prompt
board_state_dict = game.get_state()
@ -261,17 +284,57 @@ class DiplomacyAgent:
current_relationships_str = json.dumps(self.relationships)
current_goals_str = json.dumps(self.goals)
formatted_diary = self.format_private_diary_for_prompt()
# Do aggressive preprocessing of the template to fix the problematic patterns
# This includes removing any newlines or whitespace before JSON keys that cause issues
for pattern in ['negotiation_summary', 'updated_relationships', 'relationship_updates', 'intent']:
# Fix the "\n "key"" pattern that breaks .format()
prompt_template_content = re.sub(
fr'\n\s*"{pattern}"',
f'"{pattern}"',
prompt_template_content
)
# Escape all curly braces in JSON examples to prevent format() from interpreting them
# First, temporarily replace the actual template variables
temp_vars = ['power_name', 'current_phase', 'messages_this_round', 'agent_goals',
'agent_relationships', 'board_state_str']
for var in temp_vars:
prompt_template_content = prompt_template_content.replace(f'{{{var}}}', f'<<{var}>>')
# Now escape all remaining braces (which should be JSON)
prompt_template_content = prompt_template_content.replace('{', '{{')
prompt_template_content = prompt_template_content.replace('}', '}}')
# Restore the template variables
for var in temp_vars:
prompt_template_content = prompt_template_content.replace(f'<<{var}>>', f'{{{var}}}')
# Create a dictionary with safe values for formatting
format_vars = {
"power_name": self.power_name,
"current_phase": game.current_short_phase,
"board_state_str": board_state_str,
"messages_this_round": messages_this_round,
"agent_relationships": current_relationships_str,
"agent_goals": current_goals_str,
"allowed_relationships_str": ", ".join(ALLOWED_RELATIONSHIPS),
"private_diary_summary": formatted_diary
}
# Now try to use the template after preprocessing
try:
# Apply format with our set of variables
full_prompt = prompt_template_content.format(**format_vars)
logger.info(f"[{self.power_name}] Successfully formatted prompt template after preprocessing.")
success_status = "Using prompt file with preprocessing"
except KeyError as e:
logger.error(f"[{self.power_name}] Error formatting negotiation diary prompt template: {e}. Skipping diary entry.")
success_status = "Failure: Template formatting error"
return # Exit early if prompt formatting fails
logger.debug(f"[{self.power_name}] Negotiation diary prompt:\n{full_prompt[:500]}...")
full_prompt = prompt_template_content.format(
power_name=self.power_name,
current_phase=game.current_short_phase,
board_state_str=board_state_str, # Corrected to match prompt placeholder
messages_this_round=messages_this_round,
agent_relationships=current_relationships_str, # Corrected to match prompt placeholder
agent_goals=current_goals_str, # Corrected to match prompt placeholder
private_diary_summary=formatted_diary,
allowed_relationships_str=", ".join(ALLOWED_RELATIONSHIPS)
)
logger.debug(f"[{self.power_name}] Negotiation diary prompt:\n{full_prompt[:500]}...")
@ -300,17 +363,28 @@ class DiplomacyAgent:
relationships_updated = False
if parsed_data:
# Correctly get 'negotiation_summary' as requested by the prompt
diary_text_candidate = parsed_data.get('negotiation_summary')
if isinstance(diary_text_candidate, str) and diary_text_candidate.strip():
diary_entry_text = diary_text_candidate # Use the valid summary
logger.info(f"[{self.power_name}] Successfully extracted 'negotiation_summary' for diary.")
# Fix 1: Be more robust about extracting the negotiation_summary field
diary_text_candidate = None
for key in ['negotiation_summary', 'summary', 'diary_entry']:
if key in parsed_data and isinstance(parsed_data[key], str) and parsed_data[key].strip():
diary_text_candidate = parsed_data[key].strip()
logger.info(f"[{self.power_name}] Successfully extracted '{key}' for diary.")
break
if diary_text_candidate:
diary_entry_text = diary_text_candidate
else:
logger.warning(f"[{self.power_name}] 'negotiation_summary' missing or invalid in diary response. Using fallback. Value: {diary_text_candidate}")
logger.warning(f"[{self.power_name}] Could not find valid summary field in diary response. Using fallback.")
# Keep the default fallback text
# Update relationships if provided and valid
new_relationships = parsed_data.get('updated_relationships')
# Fix 2: Be more robust about extracting relationship updates
new_relationships = None
for key in ['relationship_updates', 'updated_relationships', 'relationships']:
if key in parsed_data and isinstance(parsed_data[key], dict):
new_relationships = parsed_data[key]
logger.info(f"[{self.power_name}] Successfully extracted '{key}' for relationship updates.")
break
if isinstance(new_relationships, dict):
valid_new_rels = {}
for p, r in new_relationships.items():
@ -371,6 +445,7 @@ class DiplomacyAgent:
"""
logger.info(f"[{self.power_name}] Generating order diary entry for {game.current_short_phase}...")
# Load the template but we'll use it carefully with string interpolation
prompt_template = _load_prompt_file('order_diary_prompt.txt')
if not prompt_template:
logger.error(f"[{self.power_name}] Could not load order_diary_prompt.txt. Skipping diary entry.")
@ -384,14 +459,47 @@ class DiplomacyAgent:
goals_str = "\n".join([f"- {g}" for g in self.goals]) if self.goals else "None"
relationships_str = "\n".join([f"- {p}: {s}" for p, s in self.relationships.items()]) if self.relationships else "None"
prompt = prompt_template.format(
power_name=self.power_name,
current_phase=game.current_short_phase,
orders_list_str=orders_list_str,
board_state_str=board_state_str,
agent_goals=goals_str,
agent_relationships=relationships_str
)
# Do aggressive preprocessing on the template file
# Fix any whitespace or formatting issues that could break .format()
for pattern in ['order_summary']:
prompt_template = re.sub(fr'\n\s*"{pattern}"', f'"{pattern}"', prompt_template)
# Escape all curly braces in JSON examples to prevent format() from interpreting them
# First, temporarily replace the actual template variables
temp_vars = ['power_name', 'current_phase', 'orders_list_str', 'board_state_str',
'agent_goals', 'agent_relationships']
for var in temp_vars:
prompt_template = prompt_template.replace(f'{{{var}}}', f'<<{var}>>')
# Now escape all remaining braces (which should be JSON)
prompt_template = prompt_template.replace('{', '{{')
prompt_template = prompt_template.replace('}', '}}')
# Restore the template variables
for var in temp_vars:
prompt_template = prompt_template.replace(f'<<{var}>>', f'{{{var}}}')
# Create a dictionary of variables for template formatting
format_vars = {
"power_name": self.power_name,
"current_phase": game.current_short_phase,
"orders_list_str": orders_list_str,
"board_state_str": board_state_str,
"agent_goals": goals_str,
"agent_relationships": relationships_str
}
# Try to use the template with proper formatting
try:
prompt = prompt_template.format(**format_vars)
logger.info(f"[{self.power_name}] Successfully formatted order diary prompt template.")
except KeyError as e:
logger.error(f"[{self.power_name}] Error formatting order diary template: {e}. Skipping diary entry.")
return # Exit early if prompt formatting fails
logger.debug(f"[{self.power_name}] Order diary prompt:\n{prompt[:300]}...")
response_data = None
raw_response = None # Initialize raw_response

View file

@ -260,7 +260,7 @@ class BaseModelClient:
# If all attempts failed
return None
def _validate_orders(
self, moves: List[str], possible_orders: Dict[str, List[str]]
) -> Tuple[List[str], List[str]]: # MODIFIED RETURN TYPE
@ -277,9 +277,8 @@ class BaseModelClient:
logger.debug(f"[{self.model_name}] Moves not a list, fallback.")
# Return fallback and empty list for invalid_moves_found as no specific LLM moves were processed
return self.fallback_orders(possible_orders), []
for move in moves:
move_str = move.strip()
for move_str in moves:
# Check if it's in possible orders
if any(move_str in loc_orders for loc_orders in possible_orders.values()):
validated.append(move_str)
@ -374,8 +373,25 @@ class BaseModelClient:
agent_relationships=agent_relationships,
agent_private_diary=agent_private_diary_str, # Pass diary string
)
# Get recent messages targeting this power to prioritize responses
recent_messages_to_power = game_history.get_recent_messages_to_power(power_name, limit=3)
# Debug logging to verify messages
logger.info(f"[{power_name}] Found {len(recent_messages_to_power)} high priority messages to respond to")
if recent_messages_to_power:
for i, msg in enumerate(recent_messages_to_power):
logger.info(f"[{power_name}] Priority message {i+1}: From {msg['sender']} in {msg['phase']}: {msg['content'][:50]}...")
# Add a section for unanswered messages
unanswered_messages = "\n\nRECENT MESSAGES REQUIRING YOUR ATTENTION:\n"
if recent_messages_to_power:
for msg in recent_messages_to_power:
unanswered_messages += f"\nFrom {msg['sender']} in {msg['phase']}: {msg['content']}\n"
else:
unanswered_messages += "\nNo urgent messages requiring direct responses.\n"
return context + "\n\n" + instructions
return context + unanswered_messages + "\n\n" + instructions
async def get_planning_reply( # Renamed from get_plan to avoid conflict with get_plan in agent.py
self,
@ -513,7 +529,7 @@ class BaseModelClient:
except json.JSONDecodeError as jde:
json_decode_error_occurred = True
logger.warning(f"[{self.model_name}] Failed to decode JSON block {block_index} for {power_name}. Error: {jasde}. Block content:\n{block}")
logger.warning(f"[{self.model_name}] Failed to decode JSON block {block_index} for {power_name}. Error: {jde}. Block content:\n{block}")
if parsed_messages:
success_status = "Success: Messages extracted"

View file

@ -186,6 +186,40 @@ class GameHistory:
return messages_str.strip()
# New method to get recent messages TO a specific power
def get_recent_messages_to_power(self, power_name: str, limit: int = 3) -> List[Dict[str, str]]:
"""
Gets the most recent messages sent TO this power, useful for tracking messages that need replies.
Returns a list of dictionaries with 'sender', 'content', and 'phase' keys.
"""
if not self.phases:
return []
# Get the most recent 2 phases including current phase
recent_phases = self.phases[-2:] if len(self.phases) >= 2 else self.phases[-1:]
# Collect all messages sent TO this power
messages_to_power = []
for phase in recent_phases:
for msg in phase.messages:
# Personal messages to this power or global messages from others
if msg.recipient == power_name or (msg.recipient == "GLOBAL" and msg.sender != power_name):
# Skip if sender is this power (don't need to respond to own messages)
if msg.sender != power_name:
messages_to_power.append({
'sender': msg.sender,
'content': msg.content,
'phase': phase.name
})
# Add debug logging
logger.info(f"Found {len(messages_to_power)} messages to {power_name} across {len(recent_phases)} phases")
if not messages_to_power:
logger.info(f"No messages found for {power_name} to respond to")
# Take the most recent 'limit' messages
return messages_to_power[-limit:] if messages_to_power else []
# MODIFIED METHOD (renamed from get_game_history)
def get_previous_phases_history(
self, power_name: str, current_phase_name: str, include_plans: bool = True, num_prev_phases: int = 5

View file

@ -2,17 +2,21 @@ NEGOTIATION MESSAGES
TASK
Generate one or more strategic messages to advance your interests.
Always prioritize responding to the messages in the "RECENT MESSAGES REQUIRING YOUR ATTENTION" section.
Consider:
- Your current goals
- Relationships with other powers
- Ongoing conversations
- Ongoing conversations and the need to maintain consistent threads
- Messages that need direct responses in the "REQUIRING YOUR ATTENTION" section
Message purposes can include:
- Proposing alliances
- Issuing warnings
- Gathering information
- Coordinating moves
- Strategic deception
- Responding to specific requests or inquiries (highest priority)
- Proposing alliances or support moves
- Issuing warnings or making threats
- Gathering intelligence about other powers' intentions
- Coordinating moves and suggesting tactical options
- Strategic deception when appropriate to your goals
RESPONSE FORMAT
Return ONLY JSON objects. One or more messages, each as a separate JSON object.
@ -75,9 +79,21 @@ EXAMPLES
"content": "My friend, your northern ports are looking rather exposed. While my public stance is one of general peace, perhaps we could discuss ways to ensure *your* security in the region? I have no desire for conflict with you, but an unguarded St. Petersburg is a tempting target for others. Maybe a mutual understanding could be beneficial?"
}
6. Direct response to a specific proposal (as Italy responding to Austria's question about stability in the region):
{
"message_type": "private",
"recipient": "AUSTRIA",
"content": "Thank you for your inquiry about regional stability. Regarding your concerns about Tyrolia and Trieste, I want to assure you that my army in Venice has purely defensive intentions. I agree that a peaceful southern flank benefits us both. In fact, I would propose we formalize this with a demilitarized zone agreement along our border, allowing both of us to focus elsewhere. Would you be amenable to such an arrangement?"
}
Your response must contain at least one valid JSON message block.
- Ensure recipient names are spelled correctly if sending private messages.
- Think strategically about *why* you are sending each message and what outcome you hope to achieve.
- When responding to a message, explicitly acknowledge what was said and reference specific points.
- For ongoing conversations, maintain thread continuity by referencing previous exchanges.
- If another power has made a specific proposal or request, address it directly in your response.
- When making agreements, be clear about what you are committing to and what you expect in return.
- If you need to quote something, only use single quotes in the actual messages so as not to interfere with the JSON structure.
</ImportantReminders>
JSON ONLY BELOW (DO NOT PREPEND WITH ```json or ``` or any other text)

View file

@ -23,15 +23,14 @@ Analyze the negotiations, goals, relationships, and game state to:
RESPONSE FORMAT
Return ONLY a JSON object with this structure:
{{
"negotiation_summary": "Key outcomes from negotiations",
"intent": "Strategic intent for upcoming orders",
"relationship_updates": {{
"POWER_NAME": "Enemy|Unfriendly|Neutral|Friendly|Ally"
...
}}
}}
{
"negotiation_summary": "Key outcomes from negotiations",
"intent": "Strategic intent for upcoming orders",
"updated_relationships": {
"POWER_NAME": "Enemy|Unfriendly|Neutral|Friendly|Ally"
}
}
Do not include any text outside the JSON.
@ -41,31 +40,32 @@ EXAMPLES:
Scenario 1: As France, after discussing a joint move against Germany with England, while Italy seems to be posturing aggressively in Piedmont.
{
"negotiation_summary": "Reached a tentative agreement with England to support their fleet into Belgium (BEL) if they support my army into Ruhr (RUH). Italy's messages are vague but their army in Piedmont (PIE) is concerning; they claim it's defensive against Austria but it also threatens Marseilles (MAR). Russia remains silent. Austria and Turkey are focused on each other.",
"intent": "Secure Ruhr with English support. Hold Marseilles defensively. Probe Italy's intentions further. If England upholds their end, improve relations. If Italy moves on MAR, downgrade relations severely.",
"relationship_updates": {
"ENGLAND": "Friendly",
"GERMANY": "Enemy",
"ITALY": "Unfriendly",
"AUSTRIA": "Neutral",
"RUSSIA": "Neutral",
"TURKEY": "Neutral"
}
"negotiation_summary": "Reached a tentative agreement with England to support their fleet into Belgium (BEL) if they support my army into Ruhr (RUH). Italy's messages are vague but their army in Piedmont (PIE) is concerning; they claim it's defensive against Austria but it also threatens Marseilles (MAR). Russia remains silent. Austria and Turkey are focused on each other.",
"intent": "Secure Ruhr with English support. Hold Marseilles defensively. Probe Italy's intentions further. If England upholds their end, improve relations. If Italy moves on MAR, downgrade relations severely.",
"updated_relationships": {
"ENGLAND": "Friendly",
"GERMANY": "Enemy",
"ITALY": "Unfriendly",
"AUSTRIA": "Neutral",
"RUSSIA": "Neutral",
"TURKEY": "Neutral"
}
}
Scenario 2: As Turkey, after Germany proposed an alliance against Russia, but France also offered a non-aggression pact and hinted at concerns about Austria.
{
"negotiation_summary": "Germany is keen on an anti-Russian alliance, offering support into Sevastopol (SEV) if I attack. France proposed a mutual non-aggression pact and expressed worry about Austrian expansion in the Balkans, which aligns with my concerns. England is distant. Italy seems focused on France.",
"intent": "Prioritize securing Black Sea (BLA) and consider options against Russia, but German support needs to be concrete. Maintain neutrality with France for now, as their non-aggression pact could be useful if Austria becomes a larger threat. Try to confirm German commitment before moving on Russia. Delay any aggressive moves against Austria until my position is stronger.",
"relationship_updates": {
"GERMANY": "Friendly",
"RUSSIA": "Unfriendly",
"FRANCE": "Neutral",
"ENGLAND": "Neutral",
"ITALY": "Neutral",
"AUSTRIA": "Unfriendly"
}
"negotiation_summary": "Germany is keen on an anti-Russian alliance, offering support into Sevastopol (SEV) if I attack. France proposed a mutual non-aggression pact and expressed worry about Austrian expansion in the Balkans, which aligns with my concerns. England is distant. Italy seems focused on France.",
"intent": "Prioritize securing Black Sea (BLA) and consider options against Russia, but German support needs to be concrete. Maintain neutrality with France for now, as their non-aggression pact could be useful if Austria becomes a larger threat. Try to confirm German commitment before moving on Russia. Delay any aggressive moves against Austria until my position is stronger.",
"updated_relationships": {
"GERMANY": "Friendly",
"RUSSIA": "Unfriendly",
"FRANCE": "Neutral",
"ENGLAND": "Neutral",
"ITALY": "Neutral",
"AUSTRIA": "Unfriendly"
}
}
Reminder: If you need to quote something, only use single quotes in the actual messages so as not to interfere with the JSON structure.
JSON ONLY BELOW (DO NOT PREPEND WITH ```json or ``` or any other text)

View file

@ -20,8 +20,8 @@ Write a concise diary note summarizing your orders.
RESPONSE FORMAT
Return ONLY a JSON object with this structure:
{{
"order_summary": "Brief summary of orders and strategic intent"
}}
{
"order_summary": "Brief summary of orders and strategic intent"
}
Do not include any text outside the JSON.

View file

@ -32,7 +32,7 @@ def assign_models_to_powers() -> Dict[str, str]:
"""
# POWER MODELS
"""
return {
"AUSTRIA": "claude-3-7-sonnet-20250219",
"ENGLAND": "openrouter-google/gemini-2.5-flash-preview",
@ -42,9 +42,10 @@ def assign_models_to_powers() -> Dict[str, str]:
"RUSSIA": "openrouter-deepseek/deepseek-chat-v3-0324",
"TURKEY": "openrouter-x-ai/grok-3-beta",
}
"""
# TEST MODELS
"""
return {
"AUSTRIA": "openrouter-google/gemini-2.5-flash-preview",
"ENGLAND": "openrouter-google/gemini-2.5-flash-preview",
@ -54,7 +55,7 @@ def assign_models_to_powers() -> Dict[str, str]:
"RUSSIA": "openrouter-google/gemini-2.5-flash-preview",
"TURKEY": "openrouter-google/gemini-2.5-flash-preview",
}
"""
def gather_possible_orders(game: Game, power_name: str) -> Dict[str, List[str]]:
"""