|
|
import os |
|
|
import sys |
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) |
|
|
|
|
|
import ast |
|
|
import json |
|
|
import logging |
|
|
import warnings |
|
|
from pathlib import Path |
|
|
|
|
|
import gradio as gr |
|
|
import spaces |
|
|
|
|
|
from unpredictable_lord.chat.chat import chat_with_mcp_tools |
|
|
from unpredictable_lord.chat.mcp_client import MCPClient |
|
|
from unpredictable_lord.mcp_server.game_state import PERSONALITY_DESCRIPTIONS |
|
|
from unpredictable_lord.mcp_server.mcp_server import ( |
|
|
execute_turn, |
|
|
get_game_state, |
|
|
init_game, |
|
|
list_available_advice, |
|
|
) |
|
|
from unpredictable_lord.settings import ( |
|
|
ENABLE_CHAT, |
|
|
ENABLE_GRADIO_DEPRECATION_WARNING, |
|
|
LOGGING_LEVEL, |
|
|
) |
|
|
from unpredictable_lord.utils import get_tool_prefix, update_guide_with_tool_prefix |
|
|
|
|
|
|
|
|
log_level = getattr(logging, LOGGING_LEVEL, logging.INFO) |
|
|
logging.basicConfig(level=log_level, format="%(asctime)s %(levelname)s %(message)s") |
|
|
|
|
|
|
|
|
logging.getLogger("httpx").setLevel(logging.WARNING) |
|
|
|
|
|
|
|
|
if not ENABLE_GRADIO_DEPRECATION_WARNING: |
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*Gradio.*") |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
logger.info(f"Gradio version: {gr.__version__}") |
|
|
logger.info(f"ZeroGPU: {spaces.config.Config.zero_gpu}") |
|
|
|
|
|
|
|
|
NO_GAME_MESSAGE = """_No active game._ |
|
|
|
|
|
**To start playing:** |
|
|
1. Select a Lord Personality above |
|
|
2. Click "โ๏ธ Start New Game" |
|
|
""" |
|
|
|
|
|
|
|
|
MCP_GUIDE_PATH = Path(__file__).parent / "docs" / "mcp_guide.md" |
|
|
MCP_GUIDE_TEMPLATE = MCP_GUIDE_PATH.read_text(encoding="utf-8") |
|
|
|
|
|
|
|
|
tool_prefix = get_tool_prefix() |
|
|
logger.info(f"Using tool prefix: '{tool_prefix}'") |
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="Unpredictable Lord") as demo: |
|
|
gr.Markdown( |
|
|
"# Unpredictable Lord\n\n" |
|
|
"**Lord Advisor AI Simulation** ๐ฐ๐ฎ<br>" |
|
|
"๐ฏ **Overview:** A turn-based advisor game where players propose policies and counsel a lord AI. ๐ค<br>" |
|
|
"โ๏ธ Decisions depend on lord personality, advisor trust, and current state โ affecting resources, population, and morale. ๐๐<br>" |
|
|
"๐ Win by expanding territory, earning royal favor, or growing wealth; โ ๏ธ beware rebellion, exile, or bankruptcy. ๐๐๐ฐ" |
|
|
) |
|
|
|
|
|
|
|
|
chat_session_id = gr.State(value=None) |
|
|
chat_system_instructions = gr.State(value="") |
|
|
|
|
|
SELECTED_TAB = "chat" if ENABLE_CHAT else "mcp_server" |
|
|
|
|
|
with gr.Tabs(selected=SELECTED_TAB): |
|
|
|
|
|
with gr.TabItem("Chat", id="chat", visible=ENABLE_CHAT): |
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
chatbot = gr.Chatbot(label="Lord AI", height=500, type="messages") |
|
|
|
|
|
with gr.Row(): |
|
|
msg = gr.Textbox( |
|
|
label="Your Advice", |
|
|
placeholder="Start a new game first...", |
|
|
scale=4, |
|
|
interactive=False, |
|
|
) |
|
|
submit_btn = gr.Button("Submit", scale=1, interactive=False) |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### ๐ฎ Game Status") |
|
|
|
|
|
|
|
|
with gr.Group(): |
|
|
chat_personality = gr.Dropdown( |
|
|
choices=["cautious", "idealist", "populist"], |
|
|
value="cautious", |
|
|
label="Lord Personality", |
|
|
) |
|
|
start_game_btn = gr.Button( |
|
|
"โ๏ธ Start New Game", variant="primary" |
|
|
) |
|
|
reset_game_btn = gr.Button("๐ Reset Game", variant="secondary") |
|
|
|
|
|
|
|
|
game_status_md = gr.Markdown(value=NO_GAME_MESSAGE) |
|
|
|
|
|
|
|
|
with gr.Group(): |
|
|
territory_bar = gr.Slider( |
|
|
label="๐ฐ Territory", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
population_bar = gr.Slider( |
|
|
label="๐ฅ Population", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
treasury_bar = gr.Slider( |
|
|
label="๐ฐ Treasury", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
satisfaction_bar = gr.Slider( |
|
|
label="๐ Satisfaction", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
royal_trust_bar = gr.Slider( |
|
|
label="๐ Royal Trust", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
advisor_trust_bar = gr.Slider( |
|
|
label="๐ค Advisor Trust", |
|
|
minimum=0, |
|
|
maximum=100, |
|
|
value=50, |
|
|
interactive=False, |
|
|
) |
|
|
|
|
|
|
|
|
async def start_chat_game(personality: str): |
|
|
"""Start a new game via MCP client and return updated UI state.""" |
|
|
|
|
|
mcp_client = MCPClient(tool_prefix=tool_prefix) |
|
|
result_json = await mcp_client.call_tool( |
|
|
"init_game", {"personality": personality} |
|
|
) |
|
|
|
|
|
|
|
|
try: |
|
|
result = json.loads(result_json) |
|
|
except json.JSONDecodeError: |
|
|
|
|
|
result = ast.literal_eval(result_json) |
|
|
|
|
|
session_id = result.get("session_id", "") |
|
|
system_instructions = result.get("system_instructions", "") |
|
|
state = result.get("initial_state", {}) |
|
|
personality_name = personality.capitalize() |
|
|
personality_desc = PERSONALITY_DESCRIPTIONS.get(personality, "") |
|
|
|
|
|
status_md = f"""**Turn:** {state.get("turn", 1)} | **Lord:** {personality_name} |
|
|
|
|
|
_{personality_desc}_""" |
|
|
|
|
|
|
|
|
initial_state_text = f"""**Session ID:** {session_id} |
|
|
**Personality:** {personality_name} |
|
|
|
|
|
**Initial State:** |
|
|
- Territory: {state.get("territory", 50)} |
|
|
- Population: {state.get("population", 50)} |
|
|
- Treasury: {state.get("treasury", 50)} |
|
|
- Satisfaction: {state.get("satisfaction", 50)} |
|
|
- Royal Trust: {state.get("royal_trust", 50)} |
|
|
- Advisor Trust: {state.get("advisor_trust", 50)} |
|
|
|
|
|
{system_instructions} |
|
|
""" |
|
|
initial_history = [ |
|
|
gr.ChatMessage( |
|
|
role="user", |
|
|
content=initial_state_text, |
|
|
metadata={"title": "๐ฎ Game Initialized", "status": "done"}, |
|
|
) |
|
|
] |
|
|
|
|
|
return ( |
|
|
session_id, |
|
|
system_instructions, |
|
|
status_md, |
|
|
state.get("territory", 50), |
|
|
state.get("population", 50), |
|
|
state.get("treasury", 50), |
|
|
state.get("satisfaction", 50), |
|
|
state.get("royal_trust", 50), |
|
|
state.get("advisor_trust", 50), |
|
|
initial_history, |
|
|
gr.update( |
|
|
interactive=True, placeholder="My Lord, I have a proposal..." |
|
|
), |
|
|
gr.update(interactive=True), |
|
|
) |
|
|
|
|
|
def reset_chat_game(): |
|
|
"""Reset game state to initial values.""" |
|
|
return ( |
|
|
None, |
|
|
"", |
|
|
NO_GAME_MESSAGE, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
[], |
|
|
gr.update( |
|
|
interactive=False, placeholder="Start a new game first..." |
|
|
), |
|
|
gr.update(interactive=False), |
|
|
) |
|
|
|
|
|
def refresh_game_state(session_id: str | None): |
|
|
"""Refresh the game state display.""" |
|
|
if not session_id: |
|
|
return ( |
|
|
NO_GAME_MESSAGE, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
) |
|
|
|
|
|
result = get_game_state(session_id) |
|
|
if "error" in result: |
|
|
return ( |
|
|
f"_Error: {result['error']}_", |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
50, |
|
|
) |
|
|
|
|
|
state = result.get("state", {}) |
|
|
personality = state.get("lord_personality", "unknown").capitalize() |
|
|
turn = state.get("turn", 1) |
|
|
game_over = state.get("game_over", False) |
|
|
game_result = state.get("result") |
|
|
|
|
|
if game_over and game_result: |
|
|
result_messages = { |
|
|
"victory_territory": "๐ **VICTORY!** You achieved territorial dominance!", |
|
|
"victory_trust": "๐ **VICTORY!** You became a trusted co-ruler!", |
|
|
"victory_wealth": "๐ **VICTORY!** You achieved great wealth!", |
|
|
"defeat_rebellion": "๐ **DEFEAT!** A rebellion overthrew the lord...", |
|
|
"defeat_exile": "๐ **DEFEAT!** Exiled from the kingdom...", |
|
|
"defeat_bankruptcy": "๐ **DEFEAT!** The realm fell to bankruptcy...", |
|
|
} |
|
|
status_md = f"""**Turn:** {turn} | **Lord:** {personality} |
|
|
|
|
|
{result_messages.get(game_result, f"Game Over: {game_result}")}""" |
|
|
else: |
|
|
status_md = f"**Turn:** {turn} | **Lord:** {personality}" |
|
|
|
|
|
return ( |
|
|
status_md, |
|
|
state.get("territory", 50), |
|
|
state.get("population", 50), |
|
|
state.get("treasury", 50), |
|
|
state.get("satisfaction", 50), |
|
|
state.get("royal_trust", 50), |
|
|
state.get("advisor_trust", 50), |
|
|
) |
|
|
|
|
|
def user(user_message, history): |
|
|
|
|
|
return "", history + [{"role": "user", "content": user_message}] |
|
|
|
|
|
async def bot(history, system_instructions): |
|
|
|
|
|
user_message = history[-1]["content"] |
|
|
history_for_model = history[:-1] |
|
|
|
|
|
async for updated_history in chat_with_mcp_tools( |
|
|
user_message, history_for_model, system_instructions, tool_prefix |
|
|
): |
|
|
yield updated_history |
|
|
|
|
|
|
|
|
start_game_btn.click( |
|
|
fn=start_chat_game, |
|
|
inputs=chat_personality, |
|
|
outputs=[ |
|
|
chat_session_id, |
|
|
chat_system_instructions, |
|
|
game_status_md, |
|
|
territory_bar, |
|
|
population_bar, |
|
|
treasury_bar, |
|
|
satisfaction_bar, |
|
|
royal_trust_bar, |
|
|
advisor_trust_bar, |
|
|
chatbot, |
|
|
msg, |
|
|
submit_btn, |
|
|
], |
|
|
show_api=False, |
|
|
) |
|
|
|
|
|
reset_game_btn.click( |
|
|
fn=reset_chat_game, |
|
|
inputs=[], |
|
|
outputs=[ |
|
|
chat_session_id, |
|
|
chat_system_instructions, |
|
|
game_status_md, |
|
|
territory_bar, |
|
|
population_bar, |
|
|
treasury_bar, |
|
|
satisfaction_bar, |
|
|
royal_trust_bar, |
|
|
advisor_trust_bar, |
|
|
chatbot, |
|
|
msg, |
|
|
submit_btn, |
|
|
], |
|
|
show_api=False, |
|
|
) |
|
|
|
|
|
msg.submit( |
|
|
user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False |
|
|
).then( |
|
|
bot, |
|
|
[chatbot, chat_system_instructions], |
|
|
chatbot, |
|
|
show_api=False, |
|
|
).then( |
|
|
fn=refresh_game_state, |
|
|
inputs=chat_session_id, |
|
|
outputs=[ |
|
|
game_status_md, |
|
|
territory_bar, |
|
|
population_bar, |
|
|
treasury_bar, |
|
|
satisfaction_bar, |
|
|
royal_trust_bar, |
|
|
advisor_trust_bar, |
|
|
], |
|
|
show_api=False, |
|
|
) |
|
|
|
|
|
submit_btn.click( |
|
|
user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False |
|
|
).then( |
|
|
bot, |
|
|
[chatbot, chat_system_instructions], |
|
|
chatbot, |
|
|
show_api=False, |
|
|
).then( |
|
|
fn=refresh_game_state, |
|
|
inputs=chat_session_id, |
|
|
outputs=[ |
|
|
game_status_md, |
|
|
territory_bar, |
|
|
population_bar, |
|
|
treasury_bar, |
|
|
satisfaction_bar, |
|
|
royal_trust_bar, |
|
|
advisor_trust_bar, |
|
|
], |
|
|
show_api=False, |
|
|
) |
|
|
|
|
|
|
|
|
with gr.TabItem("MCP Server", id="mcp_server"): |
|
|
gr.Markdown(update_guide_with_tool_prefix(tool_prefix, MCP_GUIDE_TEMPLATE)) |
|
|
|
|
|
gr.Markdown("### Test: Initialize Game") |
|
|
with gr.Row(): |
|
|
personality_input = gr.Dropdown( |
|
|
choices=["cautious", "idealist", "populist"], |
|
|
value="cautious", |
|
|
label="Lord Personality", |
|
|
) |
|
|
init_btn = gr.Button("Start New Game") |
|
|
|
|
|
init_output = gr.JSON(label="Game Session Info") |
|
|
|
|
|
gr.Markdown("### Test: Get Game State") |
|
|
with gr.Row(): |
|
|
session_id_input = gr.Textbox( |
|
|
label="Session ID", |
|
|
placeholder="Enter session_id from init_game", |
|
|
) |
|
|
get_state_btn = gr.Button("Get State") |
|
|
|
|
|
state_output = gr.JSON(label="Current Game State") |
|
|
|
|
|
get_state_btn.click( |
|
|
fn=get_game_state, inputs=session_id_input, outputs=state_output |
|
|
) |
|
|
|
|
|
gr.Markdown("### Test: List Available Advice") |
|
|
list_advice_btn = gr.Button("List Advice Options") |
|
|
advice_output = gr.JSON(label="Available Advice Options") |
|
|
|
|
|
list_advice_btn.click( |
|
|
fn=list_available_advice, inputs=[], outputs=advice_output |
|
|
) |
|
|
|
|
|
gr.Markdown("### Test: Execute Turn") |
|
|
with gr.Row(): |
|
|
exec_session_id = gr.Textbox( |
|
|
label="Session ID", |
|
|
placeholder="Enter session_id", |
|
|
) |
|
|
exec_advice = gr.Dropdown( |
|
|
choices=[ |
|
|
"increase_tax", |
|
|
"decrease_tax", |
|
|
"expand_territory", |
|
|
"improve_diplomacy", |
|
|
"public_festival", |
|
|
"build_infrastructure", |
|
|
"do_nothing", |
|
|
], |
|
|
value="do_nothing", |
|
|
label="Advice", |
|
|
) |
|
|
exec_btn = gr.Button("Execute Turn") |
|
|
|
|
|
exec_output = gr.JSON(label="Turn Result") |
|
|
|
|
|
exec_btn.click( |
|
|
fn=execute_turn, |
|
|
inputs=[exec_session_id, exec_advice], |
|
|
outputs=exec_output, |
|
|
) |
|
|
|
|
|
|
|
|
init_btn.click( |
|
|
fn=init_game, |
|
|
inputs=personality_input, |
|
|
outputs=init_output, |
|
|
).then( |
|
|
fn=lambda res: [res.get("session_id")] * 2, |
|
|
inputs=init_output, |
|
|
outputs=[session_id_input, exec_session_id], |
|
|
show_api=False, |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(mcp_server=True) |
|
|
|