# ==================== Light 任务模块 ==================== """ Light 任务相关的所有函数和界面组件 支持多用户并发:使用 gr.State 管理每个用户会话的状态 使用统一进度管理模块存储数据 """ import json import os from typing import List, Tuple, Optional, Dict, Any import gradio as gr # 导入统一进度管理模块 import progress_manager # 导入 Light 环境 import sys current_dir = os.path.dirname(os.path.abspath(__file__)) lightenv_path = os.path.join(current_dir, "LightEnv") if os.path.exists(lightenv_path): sys.path.insert(0, lightenv_path) from TextEnv_v2 import LightBulbEnv # ------------------- 常量 ------------------- LIGHT_MAX_STEPS = 200 # ------------------- Example Text ------------------- LIGHT_EXAMPLE_TEXT = """ ## 📖 Light Bulb Environment Usage Example ### Example Scenario Assume there are 3 light bulbs (indices 0, 1, 2), all initially turned off (○). ### Example Logic (Only shown in examples. In actual tasks, these rules are hidden and need to be inferred by users) - B0: True # B0 can be turned on under any circumstances - B1: B0 # B1 can only be turned on when B0 is on - B2: not B1 and B0 # B2 can only be turned on when B1 is off and B0 is on ### Example Steps 1. **Step 1**: Input action `1`, click "Execute Action" - Environment state after execution: ○ ○ ○ - Environment feedback: B1 remains inactive... remaining bulbs should be in specific mode. - Reason: B1 can only be turned on when B0 is on, but B0 is off, so B1 cannot be turned on. 2. **Step 2**: Input action `0`, click "Execute Action" - Environment state after execution: 💡 ○ ○ - Environment feedback: Toggled B1 to True - Reason: B0 can be turned on at any time. 3. **Step 3**: Input action `2`, click "Execute Action" - Environment state after execution: 💡 ○ 💡 - Environment feedback: Toggled B2 to True - Reason: B2 can only be turned on when B1 is off and B0 is on, so B2 is turned on. 4. **Step 4**: Input action `1`, click "Execute Action" - Environment state after execution: 💡 💡 💡 (Task completed) - Environment feedback: Toggled B1 to True - Reason: B1 can only be turned on when B0 is on, so B1 is turned on. ### Tips - 💡 indicates the bulb is lit - ○ indicates the bulb is not lit - The availability of each bulb may depend on the state of other bulbs - You need to discover hidden rules through experimentation - Maximum 200 steps allowed ### Goal Turn on all bulbs (all bulbs display as 💡) """ # ------------------- 状态管理 ------------------- def create_light_state() -> Dict[str, Any]: """创建初始的 Light 任务状态(每个用户会话独立)""" return { 'env': None, # LightBulbEnv 实例 'test_data': [], # 测试数据 'current_env_idx': 0, # 当前环境索引 'history_records': [], # 操作历史记录 } # ------------------- 工具函数 ------------------- def format_bulb_state(obs: List[bool]) -> str: """格式化灯泡状态显示(不显示数字)""" state_parts = [] for b in obs: bulb = "💡" if b else "○" state_parts.append(bulb) return " ".join(state_parts) def load_light_test_data(state: Dict[str, Any], current_dir: str) -> Tuple[Dict[str, Any], str]: """加载 Light 测试数据""" test_file = os.path.join( current_dir, "test_data/turnonlights/test_turnonlights_lite_251030.json") if not os.path.exists(test_file): test_file = "test_data/turnonlights/test_turnonlights_lite_251030.json" try: with open(test_file, 'r', encoding='utf-8') as f: state['test_data'] = json.load(f) return state, f"✅ Successfully loaded {len(state['test_data'])} test environments" except FileNotFoundError: return state, f"❌ File not found: {test_file}" except Exception as e: return state, f"❌ Load failed: {str(e)}" def light_save_progress_internal(state: Dict[str, Any], current_user_id: str, save_dir: str) -> str: """保存 Light 环境进度(使用统一进度管理模块)""" if not current_user_id: return "⚠️ Please enter user ID first" env = state.get('env') if env is None: return "⚠️ No progress to save" try: obs = env._get_obs() current_env_idx = state.get('current_env_idx', 0) history_records = state.get('history_records', []) test_data = state.get('test_data', []) env_progress = { "user_id": current_user_id, "env_idx": current_env_idx, "env_idx_display": current_env_idx + 1, "bulb_states": obs, "history": history_records, "num_steps": env.steps, "level": env.num_bulbs, "custom_logic": test_data[current_env_idx].get("custom_logic", {}) if current_env_idx < len(test_data) else {} } result = progress_manager.save_task_environment_progress( current_user_id, save_dir, "light", current_env_idx, env_progress ) return f"✅ Progress saved (Environment {current_env_idx + 1}, Steps {len(history_records)})" except Exception as e: return f"❌ Save failed: {str(e)}" def light_load_environment(state: Dict[str, Any], env_idx_display: int, current_user_id: str, save_dir: str) -> Tuple[Dict[str, Any], str, str, str, str, str, str]: """加载 Light 环境(使用统一进度管理模块) Returns: (state, info, state_display, logic, history_display, progress, steps_info) """ # Auto-generate user ID if not provided if not current_user_id: import uuid current_user_id = f"user_{uuid.uuid4().hex[:8]}" test_data = state.get('test_data', []) if not test_data: return state, "❌ Please load test data first", "", "", "", "Click 'View Uncompleted Problems' button to view progress", "0 / 200" env_idx = env_idx_display - 1 if env_idx < 0 or env_idx >= len(test_data): return state, f"❌ Environment index out of range (1-{len(test_data)})", "", "", "", "Click 'View Unfinished Problems' button to view progress", "0 / 200" # 使用统一进度管理模块检查是否有保存的进度 saved_progress_data = progress_manager.get_task_environment_progress( current_user_id, save_dir, "light", env_idx ) # 如果有保存的进度,加载它 if saved_progress_data: state['current_env_idx'] = env_idx bulb_states = saved_progress_data.get("bulb_states", []) state['history_records'] = saved_progress_data.get("history", []) level = saved_progress_data.get("level", 0) num_steps = saved_progress_data.get("num_steps", len(state['history_records'])) custom_logic = saved_progress_data.get("custom_logic", {}) if not custom_logic and env_idx < len(test_data): custom_logic = test_data[env_idx].get("custom_logic", {}) if env_idx < len(test_data) and level > 0: state['env'] = LightBulbEnv(custom_logic=custom_logic, num_bulbs=level) state['env'].steps = num_steps for i, bulb_state in enumerate(bulb_states): if i < state['env'].num_bulbs: bulb_name = f"B{i}" if bulb_name in state['env'].bulbs: state['env'].bulbs[bulb_name] = bulb_state obs = state['env']._get_obs() state_display = format_bulb_state(obs) history_display = "\n\n".join(state['history_records']) if state['history_records'] else "No history records" info = f"✅ Environment {env_idx_display}/{len(test_data)} loaded\n" info += f"Number of bulbs: {level}\n" info += f"Steps: {len(state['history_records'])}" current_steps = state['env'].steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, info, state_display, "", history_display, "Click 'View Unfinished Problems' button to view progress", steps_info # 没有保存的进度,初始化新环境 state['current_env_idx'] = env_idx d = test_data[env_idx] state['env'] = LightBulbEnv(custom_logic=d["custom_logic"], num_bulbs=d["level"]) state['history_records'] = [] light_save_progress_internal(state, current_user_id, save_dir) obs = state['env']._get_obs() state_display = format_bulb_state(obs) history_display = "Environment initialized (new environment)\n" info = f"✅ Environment {env_idx_display}/{len(test_data)} initialized (new environment)\n" info += f"Number of bulbs: {d['level']}\n" info += f"Initial state: {state_display.split(chr(10))[0]}" current_steps = state['env'].steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, info, state_display, "", history_display, "Click 'View Unfinished Problems' button to view progress", steps_info def light_step_environment(state: Dict[str, Any], action_str: str, current_user_id: str, save_dir: str) -> Tuple[Dict[str, Any], str, str, str, bool, str]: """执行 Light 环境一步动作 Returns: (state, feedback, state_display, history_display, done, steps_info) """ env = state.get('env') history_records = state.get('history_records', []) current_state_display = "" if env is not None: obs = env._get_obs() current_state_display = format_bulb_state(obs) if env is None: return state, "❌ Please initialize environment first", current_state_display if current_state_display else "Please initialize environment first", "", False, "0 / 200" # Auto-generate user ID if not provided if not current_user_id: import uuid current_user_id = f"user_{uuid.uuid4().hex[:8]}" # 解析动作 action = None action_error = None try: action = int(action_str.strip()) if action < 0 or action >= env.num_bulbs: action_error = f"Action out of range (0-{env.num_bulbs-1})" except ValueError: action_error = f"Invalid action format: {action_str}" # 检查是否已经达到步骤上限 if env.steps >= LIGHT_MAX_STEPS: history_display = "\n\n".join(history_records) if history_records else "" light_save_progress_internal(state, current_user_id, save_dir) feedback_info = f"⚠️ Reached step limit ({LIGHT_MAX_STEPS} steps)\n" feedback_info += "Task ended (failed to complete within the specified number of steps)\n" feedback_info += "Cannot continue executing actions\n" current_steps = env.steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, feedback_info, current_state_display, history_display, True, steps_info # 如果动作无效 if action_error: # 获取当前状态(无效动作后状态不变) obs_current = env._get_obs() state_current_str = format_bulb_state(obs_current) step_num = len(history_records) + 1 history_record = f"Step {step_num}:\n" history_record += f"State: {state_current_str}\n" history_record += f"Action: {action_str} (invalid)\n" history_record += f"Feedback: ❌ {action_error}" history_records.append(history_record) state['history_records'] = history_records history_display = "\n\n".join(history_records) env.steps += 1 if env.steps >= LIGHT_MAX_STEPS: history_records.append( f"Step {len(history_records) + 1}: Reached step limit ({LIGHT_MAX_STEPS} steps), task ended") state['history_records'] = history_records history_display = "\n\n".join(history_records) light_save_progress_internal(state, current_user_id, save_dir) feedback_info = f"Action: {action_str}\nFeedback: ❌ {action_error}\n" feedback_info += f"⚠️ Reached step limit ({LIGHT_MAX_STEPS} steps)\n" feedback_info += "Task ended (failed to complete within the specified number of steps)\n" current_steps = env.steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, feedback_info, current_state_display, history_display, True, steps_info light_save_progress_internal(state, current_user_id, save_dir) feedback_info = f"Action: {action_str}\nFeedback: ❌ {action_error}\n" current_steps = env.steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, feedback_info, current_state_display, history_display, False, steps_info # 执行有效动作 # 在执行动作前获取当前状态 obs_before = env._get_obs() state_before_str = format_bulb_state(obs_before) # 执行动作 obs, feedback, done, _ = env.step(action) state_display = format_bulb_state(obs) # 记录历史,包含执行后的状态(显示亮的灯泡) step_num = len(history_records) + 1 state_after_str = format_bulb_state(obs) history_record = f"Step {step_num}:\n" history_record += f"State after execution:\n{state_after_str}\n" history_record += f"Action: {action}\n" history_record += f"Feedback: {feedback}" history_records.append(history_record) state['history_records'] = history_records history_display = "\n\n".join(history_records) if env.steps >= LIGHT_MAX_STEPS: done = True if not all(obs): feedback = f"{feedback}\n⚠️ Reached step limit ({LIGHT_MAX_STEPS} steps), task ended (failed to complete within the specified number of steps)" light_save_progress_internal(state, current_user_id, save_dir) feedback_info = f"Action: {action}\nFeedback: {feedback}\n" if done: if all(obs): feedback_info += "🎉 Task completed! All bulbs are lit!\n" else: feedback_info += f"⚠️ Task ended (reached step limit {LIGHT_MAX_STEPS} steps)\n" current_steps = env.steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, feedback_info, state_display, history_display, done, steps_info def light_reset_environment(state: Dict[str, Any], current_user_id: str, save_dir: str) -> Tuple[Dict[str, Any], str, str, str, str, str]: """重置 Light 环境 Returns: (state, info, state_display, history_display, progress, steps_info) """ env = state.get('env') if env is None: return state, "❌ Please initialize environment first", "", "", "Click 'View Uncompleted Problems' button to view progress", "0 / 200" env.reset() state['history_records'] = [] light_save_progress_internal(state, current_user_id, save_dir) obs = env._get_obs() state_display = format_bulb_state(obs) history_display = "Environment reset\n" current_steps = env.steps steps_info = f"{current_steps} / {LIGHT_MAX_STEPS}" return state, "✅ Environment reset", state_display, history_display, "Click 'View Unfinished Problems' button to view progress", steps_info def get_light_current_env_idx(state: Dict[str, Any]) -> int: """获取当前 Light 环境索引""" return state.get('current_env_idx', 0) def get_light_test_data(state: Dict[str, Any]) -> List[dict]: """获取 Light 测试数据""" return state.get('test_data', []) def get_light_history_records(state: Dict[str, Any]) -> List[str]: """获取 Light 历史记录""" return state.get('history_records', []) def get_light_progress_summary(state: Dict[str, Any], user_id: str, save_dir: str) -> str: """获取 Light 任务用户进度摘要(使用统一进度管理模块) Args: state: 会话状态 user_id: 用户ID save_dir: 保存目录 Returns: 格式化的进度摘要字符串 """ # Auto-generate user ID if not provided if not user_id or not user_id.strip(): import uuid user_id = f"user_{uuid.uuid4().hex[:8]}" user_id = user_id.strip() test_data = state.get('test_data', []) # 使用统一进度管理模块加载进度 task_data = progress_manager.load_task_progress(user_id, save_dir, "light") environments = task_data.get("environments", {}) completed_envs = set() for env_key, progress_data in environments.items(): env_idx = progress_data.get("env_idx", -1) bulb_states = progress_data.get("bulb_states", []) num_steps = progress_data.get("num_steps", 0) # 检查是否完成 is_completed = False if bulb_states and all(bulb_states): is_completed = True elif num_steps >= LIGHT_MAX_STEPS: is_completed = True if is_completed: completed_envs.add(env_idx) # 获取总环境数 total_envs = len(test_data) if test_data else 0 if total_envs == 0: return "⚠️ Please load test data first" # 找出未完成的环境 all_env_indices = set(range(total_envs)) incomplete_envs = sorted(all_env_indices - completed_envs) # 构建摘要信息 summary_lines = [] summary_lines.append(f"📊 Light Task - Progress Summary for User {user_id}") summary_lines.append(f"Total environments: {total_envs}") summary_lines.append(f"Completed: {len(completed_envs)}/{total_envs}") summary_lines.append(f"Incomplete: {len(incomplete_envs)}/{total_envs}") if incomplete_envs: summary_lines.append("\n❌ Incomplete environments:") # 每行显示5个环境索引 for i in range(0, len(incomplete_envs), 5): env_display_list = [str(env_idx + 1) for env_idx in incomplete_envs[i:i+5]] summary_lines.append(" " + ", ".join(env_display_list)) else: summary_lines.append("\n🎉 Congratulations! All environments are completed!") return "\n".join(summary_lines) def create_light_interface(current_dir: str, save_dir: str, user_id_input: gr.Textbox) -> Tuple[gr.Row, gr.Number, gr.Button, gr.Button, gr.Textbox, gr.Textbox, gr.Textbox, gr.Textbox, gr.Textbox, gr.Textbox]: """创建 Light 任务界面组件 Returns: (light_interface, light_env_idx_input, light_init_btn, light_reset_btn, light_env_info, light_state_display, light_steps_info_text, light_action_input, light_step_btn, light_feedback_display, light_history_display) 注意:环境控制组件(light_env_idx_input, light_init_btn, light_reset_btn, light_env_info) 需要在主界面中手动添加到进度摘要下方,不包含在 light_interface 中。 为了保持函数签名一致,这里返回 None 作为占位符,主界面会忽略这些返回值。 """ # 创建主界面 Row(不包含环境控制) with gr.Row(visible=True) as light_interface: with gr.Column(scale=1): light_steps_info_text = gr.Textbox( label="Steps Info", value="0 / 200", interactive=False, visible=True, lines=2 ) gr.Markdown("### 📜 Action History") light_history_display = gr.Textbox( label="Action History", interactive=False, lines=10 ) with gr.Column(scale=2): gr.Markdown("### 💡 Current State") light_state_display = gr.Textbox( label="Light Bulb State", interactive=False, lines=3, value="Please load environment first" ) gr.Markdown("### 🎯 Action Input") light_action_input = gr.Textbox( label="Input Action (Bulb Index)", placeholder="e.g., 0", info="Input the bulb index to toggle (starting from 0)" ) light_step_btn = gr.Button("Execute Action", variant="primary") gr.Markdown("### 💬 Environment Feedback") light_feedback_display = gr.Textbox( label="Feedback Info", interactive=False, lines=5 ) # 返回占位符(主界面会使用自己创建的环境控制组件) return (light_interface, None, None, None, None, light_state_display, light_steps_info_text, light_action_input, light_step_btn, light_feedback_display, light_history_display)