ryomo commited on
Commit
c37e7d2
·
1 Parent(s): 7999300

feat: enhance chat functionality with system instructions and MCP client integration

Browse files
Files changed (2) hide show
  1. app.py +52 -9
  2. src/unpredictable_lord/chat/chat.py +13 -42
app.py CHANGED
@@ -4,6 +4,8 @@ import sys
4
  # Add src directory to Python path for Hugging Face Spaces compatibility
5
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
6
 
 
 
7
  import logging
8
  from pathlib import Path
9
 
@@ -11,6 +13,7 @@ import gradio as gr
11
  import spaces
12
 
13
  from unpredictable_lord.chat.chat import chat_with_mcp_tools
 
14
  from unpredictable_lord.mcp_server.game_state import PERSONALITY_DESCRIPTIONS
15
  from unpredictable_lord.mcp_server.mcp_server import (
16
  execute_turn,
@@ -45,8 +48,9 @@ NO_GAME_MESSAGE = """_No active game._
45
  with gr.Blocks(title="Unpredictable Lord") as demo:
46
  gr.Markdown("# Unpredictable Lord\nLord Advisor AI Simulation")
47
 
48
- # State to store current session_id for Chat tab
49
  chat_session_id = gr.State(value=None)
 
50
 
51
  with gr.Tabs():
52
  # Chat Tab
@@ -130,10 +134,23 @@ with gr.Blocks(title="Unpredictable Lord") as demo:
130
  )
131
 
132
  # Helper functions for Chat tab
133
- def start_chat_game(personality: str):
134
- """Start a new game and return updated UI state."""
135
- result = init_game(personality)
 
 
 
 
 
 
 
 
 
 
 
 
136
  session_id = result.get("session_id", "")
 
137
  state = result.get("initial_state", {})
138
  personality_name = personality.capitalize()
139
  personality_desc = PERSONALITY_DESCRIPTIONS.get(personality, "")
@@ -142,8 +159,31 @@ with gr.Blocks(title="Unpredictable Lord") as demo:
142
 
143
  _{personality_desc}_"""
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  return (
146
  session_id,
 
147
  status_md,
148
  state.get("territory", 50),
149
  state.get("population", 50),
@@ -151,7 +191,7 @@ _{personality_desc}_"""
151
  state.get("satisfaction", 50),
152
  state.get("royal_trust", 50),
153
  state.get("advisor_trust", 50),
154
- [], # Clear chat history
155
  gr.update(
156
  interactive=True, placeholder="My Lord, I have a proposal..."
157
  ),
@@ -162,6 +202,7 @@ _{personality_desc}_"""
162
  """Reset game state to initial values."""
163
  return (
164
  None, # session_id
 
165
  NO_GAME_MESSAGE,
166
  50,
167
  50,
@@ -236,13 +277,13 @@ _{personality_desc}_"""
236
  # Append user message to history in messages format
237
  return "", history + [{"role": "user", "content": user_message}]
238
 
239
- async def bot(history, session_id, personality):
240
  # The last message is the user's message
241
  user_message = history[-1]["content"]
242
  history_for_model = history[:-1]
243
 
244
  async for updated_history in chat_with_mcp_tools(
245
- user_message, history_for_model, session_id, personality
246
  ):
247
  yield updated_history
248
 
@@ -252,6 +293,7 @@ _{personality_desc}_"""
252
  inputs=chat_personality,
253
  outputs=[
254
  chat_session_id,
 
255
  game_status_md,
256
  territory_bar,
257
  population_bar,
@@ -271,6 +313,7 @@ _{personality_desc}_"""
271
  inputs=[],
272
  outputs=[
273
  chat_session_id,
 
274
  game_status_md,
275
  territory_bar,
276
  population_bar,
@@ -289,7 +332,7 @@ _{personality_desc}_"""
289
  user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False
290
  ).then(
291
  bot,
292
- [chatbot, chat_session_id, chat_personality],
293
  chatbot,
294
  show_api=False,
295
  ).then(
@@ -311,7 +354,7 @@ _{personality_desc}_"""
311
  user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False
312
  ).then(
313
  bot,
314
- [chatbot, chat_session_id, chat_personality],
315
  chatbot,
316
  show_api=False,
317
  ).then(
 
4
  # Add src directory to Python path for Hugging Face Spaces compatibility
5
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
6
 
7
+ import ast
8
+ import json
9
  import logging
10
  from pathlib import Path
11
 
 
13
  import spaces
14
 
15
  from unpredictable_lord.chat.chat import chat_with_mcp_tools
16
+ from unpredictable_lord.chat.mcp_client import MCPClient
17
  from unpredictable_lord.mcp_server.game_state import PERSONALITY_DESCRIPTIONS
18
  from unpredictable_lord.mcp_server.mcp_server import (
19
  execute_turn,
 
48
  with gr.Blocks(title="Unpredictable Lord") as demo:
49
  gr.Markdown("# Unpredictable Lord\nLord Advisor AI Simulation")
50
 
51
+ # State to store current session_id and system_instructions for Chat tab
52
  chat_session_id = gr.State(value=None)
53
+ chat_system_instructions = gr.State(value="")
54
 
55
  with gr.Tabs():
56
  # Chat Tab
 
134
  )
135
 
136
  # Helper functions for Chat tab
137
+ async def start_chat_game(personality: str):
138
+ """Start a new game via MCP client and return updated UI state."""
139
+ # Call init_game via MCP client
140
+ mcp_client = MCPClient()
141
+ result_json = await mcp_client.call_tool(
142
+ "init_game", {"personality": personality}
143
+ )
144
+
145
+ # Parse result - MCP may return JSON or Python dict repr
146
+ try:
147
+ result = json.loads(result_json)
148
+ except json.JSONDecodeError:
149
+ # Gradio MCP sometimes returns Python dict repr (single quotes)
150
+ result = ast.literal_eval(result_json)
151
+
152
  session_id = result.get("session_id", "")
153
+ system_instructions = result.get("system_instructions", "")
154
  state = result.get("initial_state", {})
155
  personality_name = personality.capitalize()
156
  personality_desc = PERSONALITY_DESCRIPTIONS.get(personality, "")
 
159
 
160
  _{personality_desc}_"""
161
 
162
+ # Build initial chat message with game context (collapsible)
163
+ initial_state_text = f"""**Session ID:** {session_id}
164
+ **Personality:** {personality_name}
165
+
166
+ **Initial State:**
167
+ - Territory: {state.get("territory", 50)}
168
+ - Population: {state.get("population", 50)}
169
+ - Treasury: {state.get("treasury", 50)}
170
+ - Satisfaction: {state.get("satisfaction", 50)}
171
+ - Royal Trust: {state.get("royal_trust", 50)}
172
+ - Advisor Trust: {state.get("advisor_trust", 50)}
173
+
174
+ {system_instructions}
175
+ """
176
+ initial_history = [
177
+ gr.ChatMessage(
178
+ role="user",
179
+ content=initial_state_text,
180
+ metadata={"title": "🎮 Game Initialized", "status": "done"},
181
+ )
182
+ ]
183
+
184
  return (
185
  session_id,
186
+ system_instructions,
187
  status_md,
188
  state.get("territory", 50),
189
  state.get("population", 50),
 
191
  state.get("satisfaction", 50),
192
  state.get("royal_trust", 50),
193
  state.get("advisor_trust", 50),
194
+ initial_history,
195
  gr.update(
196
  interactive=True, placeholder="My Lord, I have a proposal..."
197
  ),
 
202
  """Reset game state to initial values."""
203
  return (
204
  None, # session_id
205
+ "", # system_instructions
206
  NO_GAME_MESSAGE,
207
  50,
208
  50,
 
277
  # Append user message to history in messages format
278
  return "", history + [{"role": "user", "content": user_message}]
279
 
280
+ async def bot(history, system_instructions):
281
  # The last message is the user's message
282
  user_message = history[-1]["content"]
283
  history_for_model = history[:-1]
284
 
285
  async for updated_history in chat_with_mcp_tools(
286
+ user_message, history_for_model, system_instructions
287
  ):
288
  yield updated_history
289
 
 
293
  inputs=chat_personality,
294
  outputs=[
295
  chat_session_id,
296
+ chat_system_instructions,
297
  game_status_md,
298
  territory_bar,
299
  population_bar,
 
313
  inputs=[],
314
  outputs=[
315
  chat_session_id,
316
+ chat_system_instructions,
317
  game_status_md,
318
  territory_bar,
319
  population_bar,
 
332
  user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False
333
  ).then(
334
  bot,
335
+ [chatbot, chat_system_instructions],
336
  chatbot,
337
  show_api=False,
338
  ).then(
 
354
  user, [msg, chatbot], [msg, chatbot], queue=False, show_api=False
355
  ).then(
356
  bot,
357
+ [chatbot, chat_system_instructions],
358
  chatbot,
359
  show_api=False,
360
  ).then(
src/unpredictable_lord/chat/chat.py CHANGED
@@ -15,7 +15,6 @@ from unpredictable_lord.chat.chat_tools import (
15
  execute_tool_calls,
16
  extract_tool_calls,
17
  )
18
- from unpredictable_lord.mcp_server.game_state import PERSONALITY_DESCRIPTIONS
19
  from unpredictable_lord.settings import USE_MODAL
20
 
21
  logger = logging.getLogger(__name__)
@@ -52,47 +51,20 @@ def _get_encoding():
52
  return oh.load_harmony_encoding(oh.HarmonyEncodingName.HARMONY_GPT_OSS)
53
 
54
 
55
- def _build_system_message(session_id: str, personality: str) -> oh.Message:
56
- """Build developer message with system prompt and tool definitions."""
57
- personality_desc = PERSONALITY_DESCRIPTIONS.get(personality, "")
58
 
59
- system_prompt = f"""You are a {personality} lord of a medieval fantasy kingdom.
60
- {personality_desc}
61
-
62
- The user is your advisor. Listen to your advisor's advice and decide whether to follow it.
63
- Speak in an arrogant but thoughtful tone befitting a medieval lord.
64
-
65
- Current game session: {session_id}
66
-
67
- When you decide to take an action based on advice, you MUST call the execute_turn tool with the session_id and the appropriate advice type.
68
- Available advice options:
69
- - increase_tax: Raise taxes (Treasury ↑, Satisfaction ↓)
70
- - decrease_tax: Lower taxes (Treasury ↓, Satisfaction ↑)
71
- - expand_territory: Military expansion (Territory ↑, Treasury ↓, risky)
72
- - improve_diplomacy: Diplomatic efforts (Royal Trust ↑, Treasury ↓)
73
- - public_festival: Hold festival (Satisfaction ↑, Treasury ↓)
74
- - build_infrastructure: Build infrastructure (Population ↑, Treasury ↓)
75
- - do_nothing: Maintain current state
76
-
77
- After calling execute_turn, summarize the results to your advisor in character.
78
-
79
- TOOL USAGE GUIDELINES:
80
- - After receiving tool results, ALWAYS respond with text (do not call more tools)
81
- - Keep the conversation flowing naturally without excessive tool calls
82
-
83
- IMPORTANT: After each action or when starting a conversation:
84
- 1. Explain the current situation and any recent changes
85
- 2. Ask your advisor what they suggest next
86
- 3. Be specific about what challenges or opportunities exist
87
-
88
- Example: "The treasury has grown, but whispers of discontent spread among the peasants.
89
- What counsel do you offer, advisor? Shall we address their grievances or press our advantage?"
90
- """
91
 
 
 
 
92
  return oh.Message.from_role_and_content(
93
  oh.Role.DEVELOPER,
94
  oh.DeveloperContent.new()
95
- .with_instructions(system_prompt)
96
  .with_function_tools(TOOL_DEFINITIONS),
97
  )
98
 
@@ -162,8 +134,7 @@ async def _stream_response(
162
  async def chat_with_mcp_tools(
163
  user_message: str,
164
  chat_history: list[dict[str, str]],
165
- session_id: str,
166
- personality: str,
167
  ) -> AsyncGenerator[list[dict], None]:
168
  """
169
  Chat with LLM with MCP tool support (async streaming version).
@@ -171,8 +142,8 @@ async def chat_with_mcp_tools(
171
  Args:
172
  user_message: User's message
173
  chat_history: Past chat history (list of dictionaries in Gradio format)
174
- session_id: Current game session ID
175
- personality: Lord's personality type
176
 
177
  Yields:
178
  updated_chat_history: Updated chat history (Gradio format)
@@ -181,7 +152,7 @@ async def chat_with_mcp_tools(
181
  encoding = _get_encoding()
182
 
183
  # Build messages
184
- messages = [_build_system_message(session_id, personality)]
185
  messages.extend(_convert_history_to_messages(chat_history))
186
  messages.append(oh.Message.from_role_and_content(oh.Role.USER, user_message))
187
 
 
15
  execute_tool_calls,
16
  extract_tool_calls,
17
  )
 
18
  from unpredictable_lord.settings import USE_MODAL
19
 
20
  logger = logging.getLogger(__name__)
 
51
  return oh.load_harmony_encoding(oh.HarmonyEncodingName.HARMONY_GPT_OSS)
52
 
53
 
54
+ def _build_developer_message(system_instructions: str) -> oh.Message:
55
+ """Build developer message with system instructions and tool definitions.
 
56
 
57
+ Args:
58
+ system_instructions: System instructions from init_game() that define
59
+ the lord's personality, game state, and roleplay rules.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ Returns:
62
+ openai-harmony Message with developer role containing instructions and tools.
63
+ """
64
  return oh.Message.from_role_and_content(
65
  oh.Role.DEVELOPER,
66
  oh.DeveloperContent.new()
67
+ .with_instructions(system_instructions)
68
  .with_function_tools(TOOL_DEFINITIONS),
69
  )
70
 
 
134
  async def chat_with_mcp_tools(
135
  user_message: str,
136
  chat_history: list[dict[str, str]],
137
+ system_instructions: str,
 
138
  ) -> AsyncGenerator[list[dict], None]:
139
  """
140
  Chat with LLM with MCP tool support (async streaming version).
 
142
  Args:
143
  user_message: User's message
144
  chat_history: Past chat history (list of dictionaries in Gradio format)
145
+ system_instructions: System instructions from init_game() that define
146
+ the lord's personality, game state, and roleplay rules.
147
 
148
  Yields:
149
  updated_chat_history: Updated chat history (Gradio format)
 
152
  encoding = _get_encoding()
153
 
154
  # Build messages
155
+ messages = [_build_developer_message(system_instructions)]
156
  messages.extend(_convert_history_to_messages(chat_history))
157
  messages.append(oh.Message.from_role_and_content(oh.Role.USER, user_message))
158