ReMe/test/cookbook/frozenlake/frozenlake_react_agent.py
jinliyl d0c9d89092
feat(memory): add ContextChecker component for context size management (#144)
* feat(memory): add ContextChecker component for context size management

* refactor(memory): restructure file-based memory tools and update imports

* docs(readme): update documentation with detailed architecture and components

* docs(readme): update Chinese documentation with enhanced memory management diagrams

* refactor(cookbook): move cookbook files to test directory and clean up docs

* docs(readme): update link path for old version documentation

* docs(readme): update documentation with improved architecture diagrams and component details

* docs(readme): update documentation with improved clarity and structure

* refactor(docs): update in-memory memory documentation

* docs(readme): add experiment reproduction link to quickstart guide
2026-03-06 23:43:42 +08:00

378 lines
13 KiB
Python

import random
import re
import time
from dataclasses import dataclass
from typing import List, Dict, Any
import gymnasium as gym
import ray
import requests
import yaml
from dotenv import load_dotenv
from gymnasium.envs.toy_text.frozen_lake import generate_random_map
from loguru import logger
from openai import OpenAI
from tqdm import tqdm
load_dotenv("../../.env")
@dataclass
class GameResult:
task_id: str
run_id: int
experiment_name: str
success: bool
steps: int
reward: float
trajectory: List[Dict]
map_config: Dict[str, Any]
@ray.remote
class FrozenLakeReactAgent:
"""A ReAct Agent for FrozenLake game with task memory learning."""
def __init__(
self,
index: int,
task_configs: List[Dict],
experiment_name: str,
model_name: str = "qwen3-8b",
temperature: float = 0.7,
max_steps: int = 50,
num_runs: int = 1,
use_task_memory: bool = False,
make_task_memory: bool = False,
):
self.index = index
self.task_configs = task_configs
self.experiment_name = experiment_name
self.model_name = model_name
self.temperature = temperature
self.max_steps = max_steps
self.num_runs = num_runs
self.use_task_memory = use_task_memory
self.make_task_memory = make_task_memory
self.llm_client = OpenAI()
self.action_map = {0: "LEFT", 1: "DOWN", 2: "RIGHT", 3: "UP"}
# Load prompts
self.prompts = self._load_prompts()
def _load_prompts(self) -> Dict[str, str]:
"""Load prompts from yaml file"""
try:
with open("frozenlake_prompts.yaml", "r", encoding="utf-8") as f:
return yaml.safe_load(f)
except FileNotFoundError:
logger.warning("Prompt file not found, using default prompts")
raise FileNotFoundError(
"Prompt file not found. Please check your current path (should be ./cook/frozenlake) and try again.",
)
def call_llm(self, messages: List[Dict]) -> str:
"""Call LLM with retry logic"""
for i in range(5):
try:
response = self.llm_client.chat.completions.create(
model=self.model_name,
messages=messages,
temperature=self.temperature,
extra_body={"enable_thinking": False},
seed=0,
)
return response.choices[0].message.content
except Exception as e:
logger.warning(f"LLM call failed (attempt {i + 1}): {e}")
time.sleep(1 + i * 2)
return "LLM call failed"
def observe_state(self, env, observation: int) -> str:
"""Convert environment observation to text description"""
desc = env.unwrapped.desc
nrow, ncol = desc.shape
# Convert to string grid
grid = [[cell.decode("utf-8") for cell in row] for row in desc]
# Get current position
row, col = observation // ncol, observation % ncol
# Create visual representation
state_text = "Current State:\n"
for i in range(nrow):
for j in range(ncol):
if i == row and j == col:
state_text += f"[{grid[i][j]}]"
else:
state_text += f" {grid[i][j]} "
state_text += "\n"
state_text += "\nLegend: S=Start, F=Frozen, H=Hole, G=Goal, []=Your Position"
return state_text
def build_system_prompt(self, is_slippery: bool) -> str:
"""Build system prompt based on game configuration"""
if is_slippery:
return self.prompts["frozenlake_sys_prompt_slippery"]
else:
return self.prompts["frozenlake_sys_prompt_no_slippery"]
def get_task_memory(self, map_desc: str, is_slippery: bool) -> str:
"""Retrieve relevant task memory from task memory service"""
if not self.use_task_memory:
return ""
try:
query = f"FrozenLake game map: {map_desc}, slippery: {is_slippery}"
base_url = "http://0.0.0.0:8002/"
workspace_id = self.experiment_name
response = requests.post(
url=base_url + "retrieve_task_memory",
json={
"workspace_id": workspace_id,
"query": query,
},
timeout=60,
)
if response.status_code == 200:
data = response.json()
return data.get("answer", "")
else:
logger.warning(f"Task memory retrieval failed: {response.status_code}")
return ""
except Exception as e:
logger.warning(f"Failed to get task memory: {e}")
return ""
def action_parser(self, response: str) -> int:
"""Parse action from LLM response"""
# Look for {"action":"X"} pattern
patterns = [
r'["\']action["\']\s*:\s*["\']([0-3])["\']',
r'"action"\s*:\s*"([0-3])"',
r"'action'\s*:\s*'([0-3])'",
r'\baction["\']?\s*[:=]\s*["\']?([0-3])',
]
for pattern in patterns:
match = re.search(pattern, response)
if match:
action = int(match.group(1))
if 0 <= action <= 3:
return action
# Random fallback
action = random.randint(0, 3)
logger.warning(f"Could not parse action from response, using random: {action}")
return action
def run_single_episode(self, task_config: Dict, run_id: int) -> GameResult:
"""Run a single episode of the game"""
map_size = task_config.get("map_size", 4)
is_slippery = task_config.get("is_slippery", True)
map_desc = task_config.get("map_desc", None)
# Create environment
env_kwargs = {
"render_mode": None,
"is_slippery": is_slippery,
}
if map_desc is not None:
env_kwargs["desc"] = map_desc
else:
env_kwargs["desc"] = generate_random_map(size=map_size)
env = gym.make("FrozenLake-v1", **env_kwargs)
# Get map description for task memory
map_str = "\n".join(["".join([cell.decode("utf-8") for cell in row]) for row in env.unwrapped.desc])
# Build messages
system_prompt = self.build_system_prompt(is_slippery)
task_memory = self.get_task_memory(map_str, is_slippery)
messages = [{"role": "system", "content": system_prompt}]
if task_memory:
memory_content = f"Here are some relevant tips from previous successful games:\n\n{task_memory}\n\nUse these tips to help you succeed."
messages.append({"role": "user", "content": memory_content})
messages.append(
{"role": "assistant", "content": "I'll use these tips to navigate the frozen lake successfully."},
)
# Initialize game
observation, info = env.reset()
trajectory = []
# Add initial state
initial_state = self.observe_state(env, observation)
messages.append({"role": "user", "content": initial_state})
success = False
total_reward = 0
for step in range(self.max_steps):
# Get action from LLM
response = self.call_llm(messages)
logger.info(response)
action = self.action_parser(response)
messages.append({"role": "assistant", "content": response})
# Take action
next_observation, reward, terminated, truncated, info = env.step(action)
total_reward += reward
done = terminated or truncated
# Record trajectory step
trajectory.append(
{
"step": step,
"state": observation,
"action": action,
"action_name": self.action_map[action],
"reward": reward,
"next_state": next_observation,
"done": done,
"llm_response": response,
},
)
if done:
if terminated and reward > 0:
success = True
result_msg = f"Success! You reached the goal in {step + 1} steps!"
else:
result_msg = f"Game over! You fell into a hole or ran out of time."
messages.append({"role": "user", "content": result_msg})
break
else:
# Continue game
next_state = self.observe_state(env, next_observation)
step_msg = f"Step {step + 1}: You moved {self.action_map[action]}. Reward: {reward}\n{next_state}"
messages.append({"role": "user", "content": step_msg})
observation = next_observation
env.close()
# Create result
map_id = task_config.get("map_id", f"unknown_{self.index}_{run_id}")
task_id = f"{task_config.get('task_type', 'test')}_map{map_id}_{run_id}"
result = GameResult(
task_id=task_id,
run_id=run_id,
experiment_name=self.experiment_name,
success=success,
steps=len(trajectory),
reward=total_reward,
trajectory=trajectory,
map_config={
"map_desc": map_str,
"map_id": map_id,
"is_slippery": is_slippery,
"map_size": map_size,
"use_task_memory": self.use_task_memory,
},
)
return result, messages
def save_task_memory(self, results: List[GameResult], messages_list: List[List[Dict]]):
"""Save successful trajectories as task memory"""
if not self.make_task_memory:
return
trajectories = []
for result, messages in zip(results, messages_list):
if result.success:
# Create trajectory for task memory service
traj = {
"messages": messages,
"score": 1.0, # Success
}
trajectories.append(traj)
else:
traj = {
"messages": messages,
"score": 0.0, # Failure
}
trajectories.append(traj)
if trajectories:
try:
base_url = "http://0.0.0.0:8002/"
workspace_id = self.experiment_name
response = requests.post(
url=base_url + "summary_task_memory",
json={
"workspace_id": workspace_id,
"trajectories": trajectories,
},
timeout=300,
)
if response.status_code == 200:
logger.info(f"Saved {len(trajectories)} trajectories as task memory")
else:
logger.warning(f"Failed to save task memory: {response.status_code}")
except Exception as e:
logger.error(f"Error saving task memory: {e}")
def execute(self) -> List[Dict]:
"""Execute all tasks"""
all_results = []
all_messages = []
for task_index, task_config in tqdm(enumerate(self.task_configs), desc="Processing tasks:"):
for run_id in range(self.num_runs):
logger.info(f"Ray {self.index}, Task {task_index}, Run {run_id}")
result, messages = self.run_single_episode(task_config, run_id)
all_results.append(result)
all_messages.append(messages)
# Convert result to dict for JSON serialization
result_dict = {
"task_id": result.task_id,
"run_id": result.run_id,
"experiment_name": result.experiment_name,
"task_completed": result.success,
"success": result.success,
"steps": result.steps,
"reward": result.reward,
"map_config": result.map_config,
"trajectory": result.trajectory,
}
all_results[-1] = result_dict
# Save task memory if needed
if self.make_task_memory:
# Convert back to GameResult objects for task memory saving
game_results = []
for i, result_dict in enumerate(all_results):
game_result = GameResult(
task_id=result_dict["task_id"],
run_id=result_dict["run_id"],
experiment_name=result_dict["experiment_name"],
success=result_dict["success"],
steps=result_dict["steps"],
reward=result_dict["reward"],
trajectory=result_dict["trajectory"],
map_config=result_dict["map_config"],
)
game_results.append(game_result)
self.save_task_memory(game_results, all_messages)
return all_results