Alejandro Arguelles commited on
Commit
4ee5784
·
1 Parent(s): 615a63b

copied good agent code

Browse files
Files changed (2) hide show
  1. agent.py +370 -236
  2. mcp_server.py +127 -124
agent.py CHANGED
@@ -1,305 +1,439 @@
1
  """
2
- Student Agent for Text Adventure Games
3
 
4
- This is your submission file. Implement the StudentAgent class to play
5
- text adventure games using the MCP server you also implement.
6
 
7
- Your agent should:
8
- 1. Connect to the MCP server via the provided client
9
- 2. Use the ReAct pattern (Thought -> Action -> Observation)
10
- 3. Call MCP tools to interact with the game
11
- 4. Maximize the game score within the step limit
12
 
13
- Required method:
14
- async def run(self, client, game, max_steps, seed, verbose) -> RunResult
 
 
15
 
16
- The 'client' is a FastMCP Client already connected to your MCP server.
17
- Use it to call tools like: await client.call_tool("play_action", {"action": "look"})
18
-
19
- Tips:
20
- - Start by looking around and understanding your environment
21
- - Keep track of visited locations to avoid loops
22
- - Pick up useful items (lamp, sword, etc.)
23
- - The seed parameter should be used to set your LLM's seed for reproducibility
24
  """
25
 
26
- import json
27
  import os
28
- import re
29
- from dataclasses import dataclass, field
30
- from typing import Optional
31
-
32
- from dotenv import load_dotenv
33
  from huggingface_hub import InferenceClient
 
34
 
35
- # Load environment variables
36
- load_dotenv()
37
 
38
- # Set USE_LOCAL_MODEL=1 in your .env to use a locally downloaded model
39
- USE_LOCAL_MODEL = os.getenv("USE_LOCAL_MODEL", "0").strip() in ("1", "true", "yes")
40
- LOCAL_MODEL_ID = os.getenv("LOCAL_MODEL_ID", "Qwen/Qwen2.5-3B-Instruct")
41
 
42
  # =============================================================================
43
- # LLM Configuration - DO NOT MODIFY
44
  # =============================================================================
45
 
46
- # Model to use (fixed for fair evaluation)
47
- LLM_MODEL = "Qwen/Qwen2.5-72B-Instruct"
48
-
49
- # Initialize the LLM client based on mode
50
- _local_pipeline = None
51
-
52
- if USE_LOCAL_MODEL:
53
- import torch
54
- from transformers import pipeline as _hf_pipeline
55
-
56
- _local_pipeline = _hf_pipeline(
57
- "text-generation",
58
- model=LOCAL_MODEL_ID,
59
- torch_dtype=torch.bfloat16,
60
- device_map="auto",
61
- )
62
- LLM_CLIENT = None
63
- else:
64
- _hf_token = os.getenv("HF_TOKEN")
65
- if not _hf_token:
66
- raise ValueError("HF_TOKEN not found. Set it in your .env file.")
67
- LLM_CLIENT = InferenceClient(token=_hf_token)
68
-
69
-
70
- def call_llm(prompt: str, system_prompt: str, seed: int, max_tokens: int = 300) -> str:
71
- """
72
- Call the LLM with the given prompt. Use this function in your agent.
73
-
74
- Args:
75
- prompt: The user prompt (current game state, history, etc.)
76
- system_prompt: The system prompt (instructions for the agent)
77
- seed: Random seed for reproducibility
78
- max_tokens: Maximum tokens in response (default: 300)
79
-
80
- Returns:
81
- The LLM's response text
82
-
83
- Example:
84
- response = call_llm(
85
- prompt="You are in a forest. What do you do?",
86
- system_prompt=SYSTEM_PROMPT,
87
- seed=42,
88
- )
89
- """
90
- messages = [
91
- {"role": "system", "content": system_prompt},
92
- {"role": "user", "content": prompt},
93
- ]
94
-
95
- if USE_LOCAL_MODEL and _local_pipeline is not None:
96
- outputs = _local_pipeline(
97
- messages,
98
- max_new_tokens=max_tokens,
99
- temperature=0.0001, # Near-deterministic (0.0 unsupported by some backends)
100
- do_sample=True,
101
- )
102
- return outputs[0]["generated_text"][-1]["content"]
103
-
104
- response = LLM_CLIENT.chat.completions.create(
105
- model=LLM_MODEL,
106
- messages=messages,
107
- temperature=0.0, # Deterministic for reproducibility
108
- max_tokens=max_tokens,
109
- seed=seed,
110
- )
111
-
112
- return response.choices[0].message.content
113
 
 
 
 
114
 
115
- @dataclass
116
- class RunResult:
117
- """Result of running the agent. Do not modify this class."""
118
- final_score: int
119
- max_score: int
120
- moves: int
121
- locations_visited: set[str]
122
- game_completed: bool
123
- error: Optional[str] = None
124
- history: list[tuple[str, str, str]] = field(default_factory=list)
125
 
126
 
127
  # =============================================================================
128
- # System Prompt - Customize this for your agent
129
  # =============================================================================
130
 
131
- SYSTEM_PROMPT = """You are playing a classic text adventure game.
 
 
 
 
132
 
133
- GOAL: Explore the world, solve puzzles, and maximize your score.
134
 
135
- AVAILABLE TOOLS (use via MCP):
136
- - play_action: Execute a game command (north, take lamp, open mailbox, etc.)
137
- - memory: Get current game state and history (if implemented)
138
- - inventory: Check what you're carrying (if implemented)
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- VALID GAME COMMANDS for play_action:
141
- - Movement: north, south, east, west, up, down, enter, exit
142
- - Objects: take <item>, drop <item>, open <thing>, close <thing>, examine <thing>
143
- - Other: look, inventory, read <thing>, turn on lamp
144
 
145
- RESPOND IN THIS EXACT FORMAT (no markdown):
146
- THOUGHT: <your reasoning about what to do next>
147
  TOOL: <tool_name>
148
- ARGS: <JSON arguments, e.g., {"action": "look"}>
149
 
150
  Example:
151
- THOUGHT: I should look around to see where I am.
152
  TOOL: play_action
153
- ARGS: {"action": "look"}
154
  """
155
 
156
 
157
  # =============================================================================
158
- # Student Agent - IMPLEMENT THIS CLASS
159
  # =============================================================================
160
 
161
- class StudentAgent:
162
  """
163
- Your ReAct agent implementation.
164
-
165
- TODO:
166
- 1. Implement the run() method with the ReAct loop
167
- 2. Parse LLM responses to extract tool calls
168
- 3. Track state and avoid loops
169
 
170
- Use the provided call_llm() function to interact with the LLM.
171
  """
172
 
173
- def __init__(self):
174
- """Initialize your agent here."""
175
- # TODO: Initialize any state tracking you need
176
- # self.history = []
177
- # self.visited_locations = set()
178
- pass
179
-
180
- async def run(
181
- self,
182
- client, # FastMCP Client connected to your MCP server
183
- game: str,
184
- max_steps: int,
185
- seed: int,
186
- verbose: bool = False,
187
- ) -> RunResult:
188
  """
189
- Run the agent for a game session.
190
 
191
  Args:
192
- client: FastMCP Client connected to your MCP server
193
- game: Name of the game being played (e.g., "zork1")
194
- max_steps: Maximum number of steps to take
195
- seed: Random seed for reproducibility (use for LLM calls)
196
- verbose: Whether to print detailed output
197
-
198
- Returns:
199
- RunResult with final score and statistics
200
  """
201
- # TODO: Implement your ReAct loop here
202
- #
203
- # Basic structure:
204
- # 1. Get initial observation (call play_action with "look")
205
- # 2. Loop for max_steps:
206
- # a. Build prompt with current observation and history
207
- # b. Call LLM to get thought and action
208
- # c. Parse the response to extract tool and args
209
- # d. Call the tool via client.call_tool(tool_name, args)
210
- # e. Update history and state
211
- # f. Check for game over
212
- # 3. Return RunResult with final statistics
213
-
214
- # Example of calling a tool:
215
- # result = await client.call_tool("play_action", {"action": "look"})
216
- # observation = result[0].text if result else "No response"
217
-
218
- # Example of calling the LLM:
219
- # response = call_llm(
220
- # prompt="Current observation: " + observation,
221
- # system_prompt=SYSTEM_PROMPT,
222
- # seed=seed,
223
- # )
224
-
225
- # Placeholder implementation - replace with your code
226
- locations_visited = set()
227
- history = []
228
- final_score = 0
229
- moves = 0
230
-
231
- # TODO: Your implementation here
232
- # ...
233
-
234
- return RunResult(
235
- final_score=final_score,
236
- max_score=350, # Zork1 max score, adjust if needed
237
- moves=moves,
238
- locations_visited=locations_visited,
239
- game_completed=False,
240
- history=history,
241
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- def _build_prompt(self, observation: str, history: list) -> str:
244
  """
245
  Build the prompt for the LLM.
246
 
247
- TODO: Implement this to create effective prompts
 
 
 
 
 
248
  """
249
- # TODO: Combine system prompt, history, and current observation
250
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  def _parse_response(self, response: str) -> tuple[str, str, dict]:
253
  """
254
- Parse LLM response to extract thought, tool name, and arguments.
255
 
256
- TODO: Implement robust parsing
257
 
258
- Returns:
259
- Tuple of (thought, tool_name, args_dict)
 
 
260
  """
261
- # TODO: Parse the response format:
262
- # THOUGHT: ...
263
- # TOOL: ...
264
- # ARGS: {...}
265
- pass
266
-
267
- def _call_llm(self, prompt: str, system_prompt: str, seed: int) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  """
269
- Call the LLM with the given prompt.
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- This is a convenience wrapper - you can also use call_llm() directly.
 
 
 
 
 
 
 
 
272
  """
273
- return call_llm(prompt, system_prompt, seed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
 
276
  # =============================================================================
277
- # For local testing
278
  # =============================================================================
279
 
280
- async def test_agent():
281
- """Test the agent locally."""
282
- from fastmcp import Client
283
 
284
- # Path to your MCP server
285
- server_path = "mcp_server.py"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- agent = StudentAgent()
288
 
289
- async with Client(server_path) as client:
290
- result = await agent.run(
291
- client=client,
292
- game="zork1",
293
- max_steps=10,
294
- seed=42,
295
- verbose=True,
296
- )
297
-
298
- print(f"\nFinal Score: {result.final_score}")
299
- print(f"Moves: {result.moves}")
300
- print(f"Locations: {result.locations_visited}")
301
 
302
 
303
  if __name__ == "__main__":
304
- import asyncio
305
- asyncio.run(test_agent())
 
1
  """
2
+ ReAct Agent Template for Text Adventure Games
3
 
4
+ This is a starter template for building a ReAct agent that plays text adventures using MCP.
 
5
 
6
+ ReAct (Reasoning + Acting) is a simple but effective agent pattern:
7
+ 1. THINK: Reason about the current situation
8
+ 2. ACT: Choose and execute a tool
9
+ 3. OBSERVE: See the result
10
+ 4. Repeat until goal is achieved
11
 
12
+ Your task is to implement:
13
+ 1. Connect to the MCP server
14
+ 2. Implement the ReAct loop
15
+ 3. Use the LLM to generate thoughts and choose actions
16
 
17
+ TODO:
18
+ 1. Set up the MCP client connection
19
+ 2. Implement the agent loop
20
+ 3. Parse LLM responses to extract tool calls
 
 
 
 
21
  """
22
 
23
+ import asyncio
24
  import os
 
 
 
 
 
25
  from huggingface_hub import InferenceClient
26
+ from dotenv import load_dotenv
27
 
28
+ # FastMCP client for connecting to MCP servers
29
+ from fastmcp import Client
30
 
 
 
 
31
 
32
  # =============================================================================
33
+ # Configuration
34
  # =============================================================================
35
 
36
+ # Load environment variables
37
+ load_dotenv()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # LLM Configuration
40
+ MODEL = os.getenv("HF_MODEL", "meta-llama/Llama-3.2-3B-Instruct")
41
+ HF_TOKEN = os.getenv("HF_TOKEN")
42
 
43
+ if not HF_TOKEN:
44
+ raise ValueError("HF_TOKEN not found. Set it in your .env file.")
 
 
 
 
 
 
 
 
45
 
46
 
47
  # =============================================================================
48
+ # System Prompt - Instructions for the LLM
49
  # =============================================================================
50
 
51
+ SYSTEM_PROMPT = """You are playing a classic text adventure game. You are extremely intelligent and effective.
52
+ You are both a good strategist and skilled at devising tactics.
53
+ You are very good at thinking outside the box when necessary,
54
+ but you are pragmatic and do not seek to be conspicuously clever,
55
+ only when necessary and with the ultimate goal of maximising the score.
56
 
57
+ GOAL: Explore the world, solve puzzles, collect treasures, and maximize your score.
58
 
59
+ AVAILABLE TOOLS will be indicated and described in the prompt.
60
+ The main tools you can use to interact with the game is
61
+ play_action
62
+
63
+ Execute a game action in the text adventure.
64
+
65
+ This is the main tool for interacting with the game.
66
+
67
+ Common commands:
68
+ - Movement: north, south, east, west, up, down
69
+ - Objects: take <item>, drop <item>, open <thing>
70
+ - Look: look, examine <thing>
71
+
72
+ Args:
73
+ action: The command to execute (e.g., 'north', 'take lamp', 'open door')
74
+
75
+
76
 
77
+ VALID GAME COMMANDS:
78
+ - Movement: north, south, east, west, up, down
79
+ - Objects: take <item>, drop <item>, open <thing>, examine <thing>
80
+ - Light: turn on lamp
81
 
82
+ RESPOND IN THIS EXACT FORMAT:
83
+ THOUGHT: <your reasoning>
84
  TOOL: <tool_name>
85
+ ARGS: <arguments as JSON, or empty {} if no args>
86
 
87
  Example:
88
+ THOUGHT: I see a container. I should open it to see what's inside.
89
  TOOL: play_action
90
+ ARGS: {"action": "open container"}
91
  """
92
 
93
 
94
  # =============================================================================
95
+ # ReAct Agent Class
96
  # =============================================================================
97
 
98
+ class ReActAgent:
99
  """
100
+ A ReAct agent that uses MCP tools to play text adventures.
 
 
 
 
 
101
 
102
+ TODO: Complete this implementation!
103
  """
104
 
105
+ def __init__(self, mcp_server_path: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  """
107
+ Initialize the agent.
108
 
109
  Args:
110
+ mcp_server_path: Path to the MCP server script
 
 
 
 
 
 
 
111
  """
112
+ self.mcp_server_path = mcp_server_path
113
+ self.llm = InferenceClient(token=HF_TOKEN)
114
+ self.history: list[dict] = []
115
+ self.available_tools = []
116
+
117
+ # Summarization state
118
+ self.summary_interval = 4
119
+ self.game_summary = "Game started. No major events yet."
120
+
121
+ async def run(self, max_steps: int = 50, verbose: bool = False):
122
+ """
123
+ Run the ReAct agent loop.
124
+
125
+ TODO: Implement the main agent loop!
126
+
127
+ Steps:
128
+ 1. Connect to MCP server using FastMCP Client
129
+ 2. Get initial observation (call play_action with "look")
130
+ 3. Loop:
131
+ a. Build prompt with current observation
132
+ b. Call LLM to get thought and tool choice
133
+ c. Parse the response
134
+ d. Execute the chosen tool via MCP
135
+ e. Update history with observation
136
+ f. Check if done
137
+ """
138
+ # TODO: Implement the agent loop
139
+ # Hint: Use `async with Client(self.mcp_server_path) as client:`
140
+
141
+ print("=" * 60)
142
+ print("Starting Text Adventure ReAct Agent")
143
+ print("=" * 60)
144
+
145
+ # Connect to the MCP server
146
+ async with Client(self.mcp_server_path) as client:
147
+ # List available tools
148
+ tools = await client.list_tools()
149
+ print(f"\nAvailable tools: {[t.name for t in tools]}")
150
+ self.available_tools = await client.list_tools() # store available tools to pass later in the prompt
151
+
152
+ # Get initial observation
153
+ result = await client.call_tool("play_action", {"action": "look"})
154
+ observation = result.content[0].text
155
+ print(f"\nInitial observation:\n{observation}\n")
156
+
157
+ # Main loop
158
+ for step in range(1, max_steps + 1):
159
+ print(f"\n{'─' * 40}")
160
+ print(f"Step {step}")
161
+ print("─" * 40)
162
+
163
+ # TODO: Build prompt for LLM
164
+ prompt = self._build_prompt(observation, verbose=verbose)
165
+
166
+ # TODO: Call a LLM
167
+ response = self._call_llm(prompt)
168
+
169
+ # TODO: Parse response to get tool and arguments
170
+ thought, tool_name, tool_args = self._parse_response(response)
171
+
172
+ print(f"\nTHOUGHT: {thought}")
173
+ print(f"TOOL: {tool_name}")
174
+ print(f"ARGS: {tool_args}")
175
+
176
+ # TODO: Execute the tool via MCP
177
+ try:
178
+ result = await client.call_tool(tool_name, tool_args)
179
+ observation = result.content[0].text
180
+ print(f"\nRESULT:\n{observation}")
181
+ except Exception as e:
182
+ observation = f"Error: {e}"
183
+ print(f"\nERROR: {e}")
184
+
185
+ # TODO: Update history
186
+ self.history.append({
187
+ "thought": thought,
188
+ "tool": tool_name,
189
+ "args": tool_args,
190
+ "result": observation
191
+ })
192
+
193
+ # Check for game over
194
+ if "GAME OVER" in observation.upper():
195
+ print("\n\nGame Over!")
196
+ break
197
+
198
+ # Periodic Summarization
199
+ if len(self.history) >= self.summary_interval:
200
+ if verbose:
201
+ print("\n" + "*" * 40)
202
+ print("SUMMARIZING HISTORY...")
203
+ print("*" * 40)
204
+
205
+ new_summary = await self._update_summary(verbose=verbose)
206
+ self.game_summary = new_summary
207
+ self.history = [] # Clear history as it's now in the summary
208
+
209
+ if verbose:
210
+ print(f"NEW SUMMARY:\n{self.game_summary}\n")
211
+ print("*" * 40 + "\n")
212
+
213
+ print("\n" + "=" * 60)
214
+ print("Agent finished")
215
+ print("=" * 60)
216
 
217
+ def _build_prompt(self, observation: str, verbose: bool = False) -> str:
218
  """
219
  Build the prompt for the LLM.
220
 
221
+ TODO: Customize this to include relevant context!
222
+
223
+ Consider including:
224
+ - Current observation
225
+ - Recent history (last few actions and results)
226
+ - Warnings about repeated actions
227
  """
228
+
229
+ # AVAILABLE TOOLS (Dynamic with Schema)
230
+ import json
231
+ tool_infos = []
232
+ for t in self.available_tools:
233
+ # We include name, description AND the input schema so the LLM knows the arguments
234
+ schema = json.dumps(t.inputSchema, indent=2) if hasattr(t, 'inputSchema') else "{}"
235
+ info = f"- TOOL: {t.name}\n DESCRIPTION: {t.description}\n ARGUMENTS SCHEMA: {schema}"
236
+ tool_infos.append(info)
237
+
238
+ tool_list = "\n\n".join(tool_infos)
239
+ TOOLS_PROMPT = f"\nAVAILABLE TOOLS:\n{tool_list}\n"
240
+
241
+
242
+
243
+ parts = []
244
+ parts.append(TOOLS_PROMPT)
245
+
246
+
247
+ # Add Game Summary
248
+ parts.append(f"GAME SUMMARY (Context):\n{self.game_summary}\n")
249
+
250
+ # Add recent history
251
+ if self.history:
252
+ parts.append("Recent actions:")
253
+ for entry in self.history[-10:]:
254
+ parts.append(f" > {entry['tool']}({entry['args']}) -> {entry['result'][:150]}...")
255
+ parts.append("")
256
+
257
+ # Current observation
258
+ parts.append(f"Current observation:\n{observation}")
259
+ parts.append("\nWhat do you do next?")
260
+
261
+ if verbose:
262
+ print("\n" + "="*20 + " FULL PROMPT CONTENT " + "="*20)
263
+ for i, part in enumerate(parts):
264
+ print(f"--- PART {i+1} ---")
265
+ print(part.strip())
266
+ print("="*61 + "\n")
267
+
268
+ return "\n".join(parts)
269
+
270
+ def _detect_loop(self) -> str:
271
+ """Analyze history for repetitive patterns and return a warning if found."""
272
+ if len(self.history) < 2:
273
+ return ""
274
+
275
+ last = self.history[-1]
276
+ prev = self.history[-2]
277
+
278
+ # Simple loop: same tool and same args
279
+ if last['tool'] == prev['tool'] and last['args'] == prev['args']:
280
+ return f"You are stuck! You just tried '{last['tool']}({last['args']})' and got the same result. DO NOT DO IT AGAIN. Use 'play_action' with a different command like 'go north', 'go south', or 'inventory'."
281
+
282
+ # Pattern loop: check for A-B-A-B (last 4 actions)
283
+ if len(self.history) >= 4:
284
+ h = self.history
285
+ if h[-1]['args'] == h[-3]['args'] and h[-2]['args'] == h[-4]['args']:
286
+ return "You are oscillating in a circle. BLOCK this pattern. Go to a new room or try a completely new interaction like 'open mailbox' (if you haven't) or 'examine house'."
287
+
288
+ return ""
289
 
290
+ def _call_llm(self, prompt: str) -> str:
291
+ """
292
+ Call the LLM to get the next action.
293
+
294
+ TODO: Customize LLM parameters if needed.
295
+ """
296
+ try:
297
+ messages = [
298
+ {"role": "system", "content": SYSTEM_PROMPT},
299
+ {"role": "user", "content": prompt}
300
+ ]
301
+
302
+ response = self.llm.chat.completions.create(
303
+ model=MODEL,
304
+ messages=messages,
305
+ temperature=0.7,
306
+ max_tokens=200,
307
+ )
308
+
309
+ if False:
310
+ # printing messages (deving)
311
+ import json
312
+ print("\n" + "="*20 + " LLM MESSAGES " + "="*20)
313
+ print(json.dumps(messages, indent=2, ensure_ascii=False))
314
+ print("="*54 + "\n")
315
+
316
+
317
+ return response.choices[0].message.content
318
+ except Exception as e:
319
+ print(f"LLM Error: {e}")
320
+ return "THOUGHT: Error occurred.\nTOOL: play_action\nARGS: {\"action\": \"look\"}"
321
+
322
  def _parse_response(self, response: str) -> tuple[str, str, dict]:
323
  """
324
+ Parse the LLM response to extract thought, tool, and arguments.
325
 
326
+ TODO: Make this more robust!
327
 
328
+ Expected format:
329
+ THOUGHT: <reasoning>
330
+ TOOL: <tool_name>
331
+ ARGS: <json args>
332
  """
333
+ import json
334
+
335
+ thought = ""
336
+ tool_name = "play_action"
337
+ tool_args = {"action": "look"}
338
+
339
+ lines = response.strip().split("\n")
340
+
341
+ for line in lines:
342
+ line_upper = line.upper().strip()
343
+
344
+ if line_upper.startswith("THOUGHT:"):
345
+ thought = line.split(":", 1)[1].strip()
346
+ elif line_upper.startswith("TOOL:"):
347
+ tool_name = line.split(":", 1)[1].strip().lower()
348
+ elif line_upper.startswith("ARGS:"):
349
+ try:
350
+ args_str = line.split(":", 1)[1].strip()
351
+ tool_args = json.loads(args_str)
352
+ except (json.JSONDecodeError, IndexError):
353
+ # Try to extract action from malformed args
354
+ if "action" in args_str.lower():
355
+ # Simple extraction for common case
356
+ tool_args = {"action": "look"}
357
+
358
+ return thought, tool_name, tool_args
359
+
360
+ async def _update_summary(self, verbose: bool = False) -> str:
361
+ """
362
+ Ask the LLM to update the game summary based on recent history.
363
  """
364
+ # Format the recent history for the summarizer
365
+ recent_text = ""
366
+ for entry in self.history:
367
+ recent_text += f"- Action: {entry['tool']} {entry['args']}\n Result: {entry['result']}\n"
368
+
369
+ summary_prompt = f"""
370
+ You are an intelligent summarizer for a text adventure agent.
371
+
372
+ CURRENT SUMMARY:
373
+ {self.game_summary}
374
+
375
+ NEW EVENTS (to be added):
376
+ {recent_text}
377
 
378
+ TASK:
379
+ Create a new, updated summary of the game so far.
380
+ - Combine the Current Summary and New Events.
381
+ - Keep it under 4 paragraphs.
382
+ - Focus on important clues, discovered locations, obtained items, and completed objectives.
383
+ - Discard repetitive navigation details (e.g. "went north, then south").
384
+
385
+ RESPONSE:
386
+ Just the new summary text, nothing else.
387
  """
388
+
389
+ try:
390
+ # We use a separate direct call logic or reuse _call_llm but we need to bypass the JSON format check
391
+ # Simpler to just call the client directly here for the specific task
392
+ messages = [{"role": "user", "content": summary_prompt}]
393
+ response = self.llm.chat.completions.create(
394
+ model=MODEL,
395
+ messages=messages,
396
+ temperature=0.5,
397
+ max_tokens=500,
398
+ )
399
+ return response.choices[0].message.content.strip()
400
+ except Exception as e:
401
+ print(f"Summarization failed: {e}")
402
+ return self.game_summary # Fallback to old summary
403
 
404
 
405
  # =============================================================================
406
+ # Main - Run the agent
407
  # =============================================================================
408
 
409
+ async def main():
410
+ """Run the ReAct agent."""
411
+ import argparse
412
 
413
+ parser = argparse.ArgumentParser(description="Run the ReAct Text Adventure Agent")
414
+ parser.add_argument(
415
+ "--server", "-s",
416
+ default="templates/mcp_server_template.py",
417
+ help="Path to the MCP server script"
418
+ )
419
+ parser.add_argument(
420
+ "--max-steps", "-n",
421
+ type=int,
422
+ default=50,
423
+ help="Maximum steps to run"
424
+ )
425
+ parser.add_argument(
426
+ "--verbose", "-v",
427
+ action="store_true",
428
+ default=False,
429
+ help="Show detailed output"
430
+ )
431
 
432
+ args = parser.parse_args()
433
 
434
+ agent = ReActAgent(args.server)
435
+ await agent.run(max_steps=args.max_steps, verbose=args.verbose)
 
 
 
 
 
 
 
 
 
 
436
 
437
 
438
  if __name__ == "__main__":
439
+ asyncio.run(main())
 
mcp_server.py CHANGED
@@ -1,27 +1,15 @@
1
  """
2
- Student MCP Server for Text Adventure Games
3
 
4
- This is your MCP server submission. Implement the tools that your agent
5
- will use to play text adventure games.
6
 
7
- Required tool:
8
- play_action(action: str) -> str
9
- Execute a game command and return the result.
10
 
11
- Recommended tools:
12
- memory() -> str
13
- Return current game state, score, and recent history.
14
-
15
- inventory() -> str
16
- Return the player's current inventory.
17
-
18
- get_map() -> str
19
- Return a map of explored locations.
20
-
21
- Test your server with:
22
- fastmcp dev submission_template/mcp_server.py
23
-
24
- Then open the MCP Inspector in your browser to test the tools interactively.
25
  """
26
 
27
  import sys
@@ -38,172 +26,187 @@ from games.zork_env import TextAdventureEnv
38
  # Create the MCP Server
39
  # =============================================================================
40
 
41
- mcp = FastMCP("Student Text Adventure Server")
 
 
42
 
43
 
44
  # =============================================================================
45
  # Game State Management
46
  # =============================================================================
47
 
48
- class GameManager:
49
  """
50
  Manages the text adventure game state.
51
 
52
- TODO: Extend this class to track:
53
- - Action history (for memory tool)
54
  - Explored locations (for mapping)
55
- - Current score and moves
56
  """
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- def __init__(self):
59
- self.env: TextAdventureEnv = None
60
- self.state = None
61
- self.game_name: str = ""
62
- # TODO: Add more state tracking
63
- # self.history: list[tuple[str, str]] = []
64
- # self.explored_locations: dict[str, set[str]] = {}
65
- # self.current_location: str = ""
66
-
67
- def initialize(self, game: str = "zork1"):
68
- """Initialize or reset the game."""
69
  self.game_name = game
70
  self.env = TextAdventureEnv(game)
71
  self.state = self.env.reset()
72
- # TODO: Reset your state tracking here
73
- return self.state.observation
74
-
75
- def step(self, action: str) -> str:
76
- """Execute an action and return the result."""
77
- if self.env is None:
78
- self.initialize()
79
 
 
 
 
 
 
 
 
 
 
80
  self.state = self.env.step(action)
 
81
 
82
- # TODO: Update your state tracking here
83
- # self.history.append((action, self.state.observation))
84
- # Update location tracking, etc.
85
 
 
 
 
 
 
86
  return self.state.observation
87
-
88
- def get_score(self) -> int:
89
- """Get current score."""
90
- return self.state.score if self.state else 0
91
-
92
- def get_moves(self) -> int:
93
- """Get number of moves taken."""
94
- return self.state.moves if self.state else 0
95
 
96
 
97
- # Global game manager
98
- _game = GameManager()
99
 
100
 
101
- def get_game() -> GameManager:
102
- """Get or initialize the game manager."""
103
  global _game
104
- if _game.env is None:
105
- # Get game from environment variable (set by evaluator)
106
- game = os.environ.get("GAME", "zork1")
107
- _game.initialize(game)
108
  return _game
109
 
110
 
111
  # =============================================================================
112
- # MCP Tools - IMPLEMENT THESE
113
  # =============================================================================
114
 
115
  @mcp.tool()
116
  def play_action(action: str) -> str:
117
  """
118
- Execute a game command and return the result.
119
 
120
  This is the main tool for interacting with the game.
121
 
 
 
 
 
 
122
  Args:
123
- action: The command to execute (e.g., "north", "take lamp", "open mailbox")
124
-
125
  Returns:
126
- The game's response to the action
127
-
128
- Valid commands include:
129
- - Movement: north, south, east, west, up, down, enter, exit
130
- - Objects: take <item>, drop <item>, open <thing>, examine <thing>
131
- - Other: look, inventory, read <thing>, turn on lamp
132
  """
 
 
133
  game = get_game()
 
134
 
135
- # TODO: You might want to add action validation here
136
- # TODO: You might want to include score changes in the response
137
-
138
- result = game.step(action)
139
-
140
- # Optional: Append score info
141
- # result += f"\n[Score: {game.get_score()} | Moves: {game.get_moves()}]"
142
-
143
  return result
144
 
145
 
146
- # TODO: Implement additional tools to help your agent
 
147
 
148
  # @mcp.tool()
149
  # def memory() -> str:
150
  # """
151
- # Get the current game state summary.
152
  #
153
- # Returns:
154
- # A summary including current location, score, moves, and recent history
155
  # """
156
- # game = get_game()
157
- # # TODO: Return useful state information
158
  # pass
159
 
160
 
161
- # @mcp.tool()
162
- # def inventory() -> str:
163
- # """
164
- # Check what the player is carrying.
165
- #
166
- # Returns:
167
- # List of items in the player's inventory
168
- # """
169
- # game = get_game()
170
- # result = game.step("inventory")
171
- # return result
172
 
173
 
174
- # @mcp.tool()
175
- # def get_map() -> str:
176
- # """
177
- # Get a map of explored locations.
178
- #
179
- # Returns:
180
- # A text representation of explored locations and connections
181
- # """
182
- # game = get_game()
183
- # # TODO: Return map of explored locations
184
- # pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
 
187
- # @mcp.tool()
188
- # def get_valid_actions() -> str:
189
- # """
190
- # Get a list of likely valid actions from the current location.
191
- #
192
- # Returns:
193
- # List of actions that might work here
194
- # """
195
- # # This is a hint: Jericho provides get_valid_actions()
196
- # game = get_game()
197
- # if game.env and game.env.env:
198
- # valid = game.env.env.get_valid_actions()
199
- # return "Valid actions: " + ", ".join(valid[:20])
200
- # return "Could not determine valid actions"
201
 
202
 
203
  # =============================================================================
204
- # Run the server
205
  # =============================================================================
206
 
207
  if __name__ == "__main__":
208
- # This runs the server with stdio transport (for MCP clients)
209
  mcp.run()
 
1
  """
2
+ MCP Server Template for Text Adventure Games
3
 
4
+ This is a starter template for building your text adventure MCP server.
5
+ Your task is to implement the tools that allow an AI agent to play text adventures.
6
 
7
+ FastMCP makes it easy to create MCP servers - just decorate functions!
 
 
8
 
9
+ TODO:
10
+ 1. Implement the play_action tool (required)
11
+ 2. Add helper tools like memory, get_map, inventory (recommended)
12
+ 3. Test your server with: fastmcp dev templates/mcp_server_template.py
 
 
 
 
 
 
 
 
 
 
13
  """
14
 
15
  import sys
 
26
  # Create the MCP Server
27
  # =============================================================================
28
 
29
+ # TODO: Create a FastMCP server instance
30
+ # Hint: mcp = FastMCP("Your Server Name")
31
+ mcp = FastMCP("Text Adventure Server")
32
 
33
 
34
  # =============================================================================
35
  # Game State Management
36
  # =============================================================================
37
 
38
+ class GameState:
39
  """
40
  Manages the text adventure game state.
41
 
42
+ TODO: You may want to extend this class to track:
43
+ - Action history (for context)
44
  - Explored locations (for mapping)
45
+ - Current location name
46
  """
47
+
48
+ def _clean_jericho_name(self, s: str) -> str:
49
+ """Clean raw Jericho string representations (e.g. 'Obj180: West House Parent...')."""
50
+ if not s: return s
51
+ if "Obj" in s and ":" in s:
52
+ # Extract name part: "Obj180: West House Parent..." -> "West House"
53
+ parts = s.split(":", 1)[1].strip()
54
+ if "Parent" in parts:
55
+ parts = parts.split("Parent")[0].strip()
56
+ return parts
57
+ return s
58
 
59
+ def __init__(self, game: str = "zork1"):
 
 
 
 
 
 
 
 
 
 
60
  self.game_name = game
61
  self.env = TextAdventureEnv(game)
62
  self.state = self.env.reset()
63
+ # Track additional state
64
+ self.history = []
65
+ self.explored_locations = {}
66
+ self.summary = "No detailed memory yet. I have just started the game."
 
 
 
67
 
68
+ # Initial State Clean
69
+ if self.state.location:
70
+ self.state.location = self._clean_jericho_name(self.state.location)
71
+ # Add initial location to map
72
+ self.explored_locations[self.state.location] = 1
73
+
74
+ def take_action(self, action: str) -> str:
75
+ """Execute a game action and return the result."""
76
+ action = action.strip()
77
  self.state = self.env.step(action)
78
+ self.history.append(action)
79
 
80
+ # Clean location
81
+ if self.state.location:
82
+ self.state.location = self._clean_jericho_name(self.state.location)
83
 
84
+ # Track explored locations
85
+ if self.state.location and self.state.location != "Unknown":
86
+ count = self.explored_locations.get(self.state.location, 0)
87
+ self.explored_locations[self.state.location] = count + 1
88
+
89
  return self.state.observation
 
 
 
 
 
 
 
 
90
 
91
 
92
+ # Global game instance (created on first use)
93
+ _game: GameState | None = None
94
 
95
 
96
+ def get_game() -> GameState:
97
+ """Get or create the game instance."""
98
  global _game
99
+ if _game is None:
100
+ _game = GameState()
 
 
101
  return _game
102
 
103
 
104
  # =============================================================================
105
+ # MCP Tools - IMPLEMENT THESE!
106
  # =============================================================================
107
 
108
  @mcp.tool()
109
  def play_action(action: str) -> str:
110
  """
111
+ Execute a game action in the text adventure.
112
 
113
  This is the main tool for interacting with the game.
114
 
115
+ Common commands:
116
+ - Movement: north, south, east, west, up, down
117
+ - Objects: take <item>, drop <item>, open <thing>
118
+ - Look: look, examine <thing>
119
+
120
  Args:
121
+ action: The command to execute (e.g., 'north', 'take lamp')
122
+
123
  Returns:
124
+ The game's response to your action
 
 
 
 
 
125
  """
126
+ # TODO: Implement this tool
127
+ # Hint: Use get_game().take_action(action)
128
  game = get_game()
129
+ result = game.take_action(action)
130
 
131
+ # TODO: Optionally add score info or game over detection
 
 
 
 
 
 
 
132
  return result
133
 
134
 
135
+ # TODO: Implement additional helper tools
136
+ # These are optional but will help your agent play better!
137
 
138
  # @mcp.tool()
139
  # def memory() -> str:
140
  # """
141
+ # Get a summary of the current game state.
142
  #
143
+ # Returns location, score, recent actions, and current observation.
144
+ # Use this to understand where you are and what happened recently.
145
  # """
146
+ # # TODO: Implement this
 
147
  # pass
148
 
149
 
150
+ @mcp.tool()
151
+ def inventory() -> str:
152
+ """List the items currently carried."""
153
+ game = get_game()
154
+ items = game.state.inventory
155
+ if not items:
156
+ return "You are not carrying anything."
157
+
158
+ # Clean item names
159
+ cleaned_items = [game._clean_jericho_name(i) for i in items]
160
+ return "Carrying:\n" + "\n".join(f"- {item}" for item in cleaned_items)
161
 
162
 
163
+ @mcp.tool()
164
+ def get_map() -> str:
165
+ """List all locations visited so far."""
166
+ game = get_game()
167
+ if not game.explored_locations:
168
+ return "You haven't explored any locations yet."
169
+
170
+ lines = ["Explored Locations:"]
171
+ for loc, count in game.explored_locations.items():
172
+ mark = " (Current)" if loc == game.state.location else ""
173
+ lines.append(f"- {loc} [Visited {count} times]{mark}")
174
+
175
+ return "\n".join(lines)
176
+
177
+
178
+ @mcp.tool()
179
+ def get_current_state() -> str:
180
+ """
181
+ Get a snapshot of the current game situation.
182
+
183
+ Returns:
184
+ - Current Observation (what you see)
185
+ - Score and Moves
186
+ - Current Memory Notebook content
187
+ """
188
+ game = get_game()
189
+ info = [
190
+ "=== CURRENT GAME STATE ===",
191
+ f"Score: {game.state.score}/{game.state.max_score}",
192
+ f"Moves: {game.state.moves}",
193
+ f"Inventory (Last Known): {[game._clean_jericho_name(i) for i in game.state.inventory]}",
194
+ "",
195
+ "--- OBSERVATION ---",
196
+ game.state.observation,
197
+ "",
198
+ "--- NOTEBOOK ---",
199
+ game.summary
200
+ ]
201
+ return "\n".join(info)
202
 
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
 
206
  # =============================================================================
207
+ # Main - Run the server
208
  # =============================================================================
209
 
210
  if __name__ == "__main__":
211
+ # This runs the server using stdio transport (for local testing)
212
  mcp.run()