Files
2025-05-23 22:01:00 +08:00

475 lines
25 KiB
Python

import random
import numpy as np
import re
import uuid
import json
import argparse
import pathlib
import os
from typing import List, Tuple
import copy
from games.base.game import Game
from base.data import Data
from games.tasks.time_sequence.scripts.time_sequence_verifier import TimeSequenceVerifier
from games.tasks.time_sequence.scripts.time_sequence_prompt import prompt_timeSequence
class TimeSequence(Game):
"""
TimeSequence任务类
"""
def __init__(self):
"""
初始化 time sequece 任务
"""
super().__init__("TimeSequence", TimeSequenceVerifier)
def generate(self, num_of_questions: int = 100, max_attempts: int = 100,
n: int = 3, rule_prob:int=50):
"""
生成 time sequence 的题目
@param num_of_questions: 要生成的题目数量
@param max_attempts: 每个题目的最大尝试次数
@param n: 题目中的人物数量
@param rule_prob: 为人物生成额外规则的概率
@return: 生成的题目列表
"""
if n <= 1:
raise ValueError("人数n必须大于1")
if rule_prob<0 or rule_prob>100:
raise ValueError("概率值必须在[0,1]之间")
rule_prob /= 100
game_data_list = []
generated_matrices = set()
attempts = 0
# 使用自定义的游戏生成类
generator = Generate_Task()
while len(game_data_list) < num_of_questions and attempts < max_attempts:
try:
attempts += 1
# 随机选择中文或英文提示
is_chinese = random.choice([True, False])
# 生成游戏相关数据
game_data = generator.generate_main(n=n, rule_prob=rule_prob, is_chinese=is_chinese)
# 将游戏数据转为字符串以检查重复
game_data_str = json.dumps(game_data)
if game_data_str in generated_matrices:
continue
generated_matrices.add(game_data_str)
# 生成问题描述
question = prompt_timeSequence(raw_schedule=game_data['raw_schedule'], rules_desc=game_data['rules_desc'], is_chinese=is_chinese)
# 创建游戏数据
game_data = Data(
question=question,
answer=[game_data['answers']['answer_maxLen'], game_data['answers']['answer_nums']], # 保存正确答案
metadata={
"trace_id": str(uuid.uuid4()),
"n": n,
"rule_prob": rule_prob,
"records": game_data
}
)
game_data_list.append(game_data)
except Exception as e:
print(f"生成题目时出错: {e}")
# 添加调试信息
import traceback
print(f"错误详情: {traceback.format_exc()}")
continue
if len(game_data_list) < num_of_questions:
print(f"警告: 只能生成 {len(game_data_list)}/{num_of_questions} 条游戏数据")
return game_data_list
def extract_answer(self, test_solution: str):
"""
从模型的回答中提取答案(矩阵)
@param test_solution: 模型的完整回答
@return: 提取答案列表
"""
if not test_solution:
return ""
# 尝试提取列表
matrix_pattern = r'\[.*?\]'
matrix_matches = re.findall(matrix_pattern, test_solution, re.DOTALL)
if matrix_matches:
# 使用最后一个匹配的列表
print(matrix_matches)
return matrix_matches[-1].strip()
# 如果失败,返回空字符串
return ""
class Generate_Task():
def __init__(self):
self.rules_desc, self.rules_desc_english = self.add_special_rules()
self.rule_names = list(self.rules_desc.keys())
pass
def generate_main(self, n:int=3, rule_prob:float=0.5, is_chinese=True):
"""
生成任务相关数据
@para n: 人物数量,数量越多,难度越大
@para rule_prob: 为人物添加额外规则的概率
@return: 返回人物名称、初始日程安排信息、规则限制、转换后的日程安排、最终答案
"""
final_result = {'raw_schedule':{}, 'trans_schedule':{}, 'rules':{}, 'rules_desc':{}, 'schedule_withRule':{}, 'answers':{}}
# 随机选择人名
choosed_names = self.choose_names(n=n, is_chinese=is_chinese)
# 为每个人物生成一周的日程安排,并进行转换
for name in choosed_names:
raw_days_schedules, trans_days_schedules = self.relation_intergration()
final_result['raw_schedule'][name] = raw_days_schedules
final_result['trans_schedule'][name] = trans_days_schedules
# print('----------------------------------')
# print(final_result)
# 为每个人物按照rule_prob的概率添加规则
for name in choosed_names:
if random.random()<=rule_prob:
choosed_rule = random.choice(self.rule_names)
final_result['rules'][name] = choosed_rule
final_result['rules_desc'][name] = self.rules_desc[choosed_rule] if is_chinese else self.rules_desc_english[choosed_rule]
else:
final_result['rules'][name] = ''
final_result['rules_desc'][name] = ''
# print('================================')
# 按照规则描述,更新日程安排,用于计算答案
for name in choosed_names:
new_schedule = self.add_special_rules(
days_schedules=final_result['trans_schedule'][name],
choosed_rule=final_result['rules'][name]
)
final_result['schedule_withRule'][name] = new_schedule
# print('*'*20)
# print(final_result)
# 根据规则,获取最长会议时间限制
time_limit = self.cal_maxLen_time(rules=final_result['rules'])
# 计算最终答案
answer_record, answer_nums, answer_maxLen = self.cal_answers(final_result['schedule_withRule'], time_limit=time_limit)
final_result['answers'] = {'answer_record':answer_record, 'answer_nums':answer_nums, 'answer_maxLen':answer_maxLen}
return final_result
def choose_names(self, n:int=3, is_chinese=True):
candidate_names_english = [
'Kaitlyn', 'Gerald', 'Jocelyn', 'Sara', 'Edwin', 'Savannah', 'Brian', 'Angela', 'Albert', 'Sean', 'Walter',
'Bailey', 'Helen', 'Sierra', 'Henry', 'Wayne', 'Kayla', 'Janice', 'Timothy', 'Tyler', 'Cindy', 'Julie', 'Tiffany',
'Robert', 'Chelsea', 'Joan', 'Benjamin', 'Raymond', 'Tammy', 'Kristen', 'Ashley', 'Mariah', 'Alyssa', 'Taylor',
'Thomas', 'Rose', 'Alexa', 'Rebecca', 'Sophia', 'Juan', 'Kenneth', 'Eric', 'Joe', 'Melanie', 'Ronnie', 'Leonard',
'Teresa', 'Patricia', 'Sydney', 'Paige', 'Morris', 'Katherine', 'Lauren', 'Judy', 'Stephen', 'Caleb', 'Gregory',
'Danny', 'Catherine', 'Martha', 'Abigail', 'Joshua', 'Carol', 'Christine', 'Jeremy', 'Arthur', 'Willie', 'Norman',
'Rodney', 'Larry', 'Luis', 'Casey', 'Amy', 'Gary', 'Philip', 'Melissa', 'Nicholas', 'Todd', 'Jean', 'Vanessa',
'Isabella', 'Ruth', 'Dylan', 'Michael', 'Roger', 'Frances', 'Craig', 'Glenn', 'Austin', 'Dale', 'Elizabeth', 'Allen',
'Deborah', 'Steven', 'Jack', 'Nathan', 'Terry', 'Theresa', 'Clarence', 'Manuel', 'Megan', 'Brittany', 'Johnny', 'Diane',
'Alfred', 'Harvey', 'Amanda', 'Emma', 'Morgan', 'Crystal', 'Tony', 'Faith', 'Samuel', 'Keith', 'Gabriel', 'Francis',
'Anthony', 'Victoria', 'Mark', 'Jesse', 'Makayla', 'Isaac', 'Allison', 'Rachel', 'Billy', 'Lindsey', 'Brenda', 'Rylee',
'Sabrina', 'Kylie', 'Stanley', 'Olivia', 'Jasmine', 'Edgar', 'Betty', 'Sandra', 'Floyd', 'James', 'Zoey', 'Susan', 'Jacob',
'Grace', 'Ronald', 'Justin', 'Randy', 'Marilyn', 'Vincent', 'Andrea', 'Autumn', 'Dorothy', 'Ernest', 'Jane', 'Brandon', 'Maria',
'Lisa', 'Patrick', 'William', 'Diana', 'Jimmy', 'Theodore', 'Carolyn', 'Alexander', 'Christina', 'Lloyd', 'Amber', 'Mikayla', 'Brooke',
'Jason', 'Samantha', 'Hugh', 'Aaron', 'Donna', 'Tommy', 'Dennis', 'Kyle', 'Kelsey', 'Erin', 'Charlotte', 'Jerome', 'Marissa',
'Alice', 'Leon', 'Jeffrey', 'Kathryn', 'Cory', 'Madeline', 'Alan', 'Carl', 'Kimberly', 'Joyce', 'Curtis', 'Michelle', 'Marvin',
'Jose', 'Daniel', 'Virginia', 'Shawn', 'Marie', 'Sarah', 'Karen', 'Earl', 'Brianna', 'Gloria', 'Lionel', 'Madison', 'John', 'Linda',
'Sharon', 'Anna', 'Cynthia', 'Beverly', 'Ann', 'Margaret', 'Denise', 'Bradley', 'Nathaniel', 'Michaela', 'Dustin', 'Logan', 'George',
'Frank', 'Donald', 'Glen', 'Stephanie', 'Shirley', 'Douglas', 'Bryan', 'Jordan', 'Bruce', 'Ryan', 'Eugene', 'Alexis', 'Andrew', 'Paul',
'Christopher', 'Nancy', 'Heather', 'Debra', 'Peter', 'Kelly', 'Danielle', 'Oscar', 'Joel', 'Otis', 'Gabriella', 'Lawrence', 'Jennifer',
'Evelyn', 'Lori', 'Hannah', 'Adam', 'Ethan', 'Kathleen', 'Cheryl', 'Emily', 'Barbara', 'Sophie', 'Pamela', 'Nicole', 'Martin', 'Zachary',
'Edward', 'Doris', 'Ralph', 'Mary', 'Noah', 'Christian', 'Russell', 'Joseph', 'Natalie', 'Jay', 'Laura', 'Harold', 'Julia', 'Jacqueline',
'Roy', 'Frederick', 'Judith', 'Matthew', 'Andre', 'Louis', 'Jessica', 'Kim', 'Valerie', 'Richard', 'Clifford', 'Amelia', 'Charles',
'Janet', 'Howard', 'Molly', 'Jonathan', 'Derek', 'David', 'Jerry', 'Jared', 'Bobby', 'Shane', 'Scott', 'Kevin', 'Haley'
]
candidate_names_chinese = [
"张伟", "王芳", "李娜", "刘洋", "陈强", "杨静", "黄磊", "赵敏", "周涛", "吴昊",
"徐磊", "孙浩", "胡娟", "朱丽", "高峰", "马超", "董倩", "冯伟", "程琳", "宋刚",
"何敏", "梁涛", "许倩", "邓超", "曹阳", "沈丽", "田野", "韩冰", "龚涛", "金鑫",
"薛伟", "贾磊", "戴琳", "孟凡", "方婷", "邵明", "于波", "姚辉", "熊杰", "郑浩",
"任涛", "陶亮", "卢伟", "白雪", "马丽", "钱峰", "汪静", "严磊", "戴强", "阎涛",
"谭敏", "贺丽", "罗杰", "夏阳", "石磊", "史娟", "姜涛", "左娜", "冉亮", "章伟",
"蒋倩", "莫凡", "丁浩", "易波", "覃峰", "尤静", "韦超", "庄敏", "庞磊", "武杰",
"管霞", "蓝涛", "皮娜", "封伟", "栗倩", "党超", "池娜", "辛伟", "昌磊", "支昊",
"边静", "黎涛", "湛亮", "丰杰", "候丽", "满伟", "滕静", "隆磊", "蔺倩", "温涛",
"雷浩", "谷超", "左丽", "宁峰", "沙倩", "郁磊", "鲍娜", "佟涛", "仇伟", "栾敏",
"甘磊", "那超", "殷静", "井伟", "伊倩", "仲涛", "乐丽", "虞峰", "沃超", "俞娜",
"靳伟", "岳涛", "郜磊", "阮倩", "党丽", "慕超", "荆娜", "花涛", "帅伟", "占浩",
"钟涛", "邝磊", "迟倩", "冶超", "拓静", "宫伟", "宓涛", "仝磊", "督倩", "郏丽",
"甄超", "幸娜", "门涛", "来伟", "覃磊", "化倩", "厚丽", "买超", "年娜", "伊涛",
"阴伟", "侯磊", "暴倩", "封丽", "白超", "乐娜", "全涛", "贝伟", "简磊", "车倩",
"敖丽", "满超", "勾娜", "类涛", "桑伟", "谭磊", "师倩", "詹丽", "池超", "原娜",
"东涛", "斐伟", "富磊", "漆倩", "达丽", "西超", "晁娜", "巨涛", "言伟", "普磊",
"翦倩", "湛丽", "年超", "霜娜", "桑涛", "邸伟", "米磊", "阿倩", "义丽", "秋超",
"区娜", "里涛", "满伟", "詹磊", "红倩", "豆丽", "纵超", "全娜", "闻涛", "赫伟",
"左磊", "岳倩", "理丽", "白涛", "鲁伟", "周磊", "冀倩", "东丽", "冷超", "党娜",
"钱涛", "冉伟", "印磊", "殷倩", "宇丽", "关超", "糜娜", "朱涛", "吉伟", "胡磊"
]
if is_chinese:
return random.sample(candidate_names_chinese, k=n)
return random.sample(candidate_names_english, k=n)
def generate_schedule(self):
"""
为单个人物随机生成日称安排表
从早上9点到晚上5点,每5分钟作为一个时间段进行分割。
@para name:任务名称
@return:一个长度为5的列表,代表周一到周五,每个元素是一个子列表,代表一天的日程安排
"""
def generate_day_schedule(max_sche_nums):
"""
随机生成一天的日程安排。将早上9点到晚上5点,分为96个时间段,每个时间段为5分钟。
result中的子列表,第一个元素为起始时间,第二个元素为结束时间。
"""
result = []
pre_end = 0
cur_nums=0
while(cur_nums<max_sche_nums):
weights = [1 / (i + 5) for i in range(36)]
start_time = pre_end + random.choices(range(36), weights=weights, k=1)[0]
weights = [1 / (i + 3) for i in range(24)]
duration = random.choices(range(1, 25), weights=weights, k=1)[0]
end_time = start_time + duration
if end_time>96:
break
result.append([start_time, end_time])
pre_end = end_time
return result
days_schedules = []
for i in range(5):
days_schedules.append(generate_day_schedule(random.choice([2,3,4,5])))
return days_schedules
def relation_intergration(self):
"""
日程安排生成与转换
"""
# 1. 为人物生成每天的日程安排
days_schedules = self.generate_schedule()
# 2. 为人物的每一天,随机选择日程类型为 空闲、占用
days_schedules_types = {}
for day, day_schedule in zip(['一', '二', '三', '四', '五'], days_schedules):
sche_type = random.choice(['空闲', '占用'])
days_schedules_types[day] = [sche_type, day_schedule]
raw_days_schedules_types = copy.deepcopy(days_schedules_types)
# 3. 将空闲时间,转为占用时间
days_schedules_types_trans = self.trans_free_2_booked(days_schedules_types)
return raw_days_schedules_types, days_schedules_types_trans
def trans_free_2_booked(self, days_schedules_types:dict):
"""
将空闲日程,转为占用日程,方便求解答案
"""
for key in days_schedules_types:
if days_schedules_types[key][0]=='空闲':
new_day_schedule = []
start_time = 0
for sche in days_schedules_types[key][1]:
new_day_schedule.append([start_time, sche[0]])
start_time = sche[1]
new_day_schedule.append([start_time, 96])
days_schedules_types[key][1] = new_day_schedule
days_schedules_types[key][0] = '占用'
return days_schedules_types
def add_special_rules(self, days_schedules:dict={}, choosed_rule:str=''):
"""
添加一些特殊的规则限制
"""
rules_english = {
'会议前的空闲时间限制': ' requires at least 10 minutes of free time before a meeting, and the free time must be between 9:00 AM and 5:00 PM.',
'会议后的空闲时间限制': ' requires at least 10 minutes of free time after a meeting, and the free time must be between 9:00 AM and 5:00 PM.',
'结束时间限制': ' feels tired and requires meetings to end before 4:00 PM.',
'星期三午饭时间': ' needs to have lunch between 12:30 PM and 1:00 PM on Wednesdays.',
'清空早上日程': ' can cancel the schedule between 9:00 AM and 9:45 AM each day (only those ending before 9:45 AM can be cancelled).',
'时区差异': ' is in a time zone that is one hour ahead of others.',
'时间偏好二四': ' prefers to hold short meetings on Tuesdays and Thursdays and only accepts new meetings on these two days, with a maximum duration of 1 hour.',
'30分钟短会议清除': ' can remove any meetings of 30 minutes or less from the schedule.',
'10分钟短会议清除': ' can be flexible and miss up to 10 minutes of a meeting.',
'时间偏好二五': ' prefers to hold long meetings on Tuesdays and Fridays and only accepts new meetings on these two days, but the meeting duration must not exceed 30 minutes.',
}
rules = {
'会议前的空闲时间限制': '要求在会议前至少有10分钟的空闲时间,并且空闲时间必须在上午9点到下午5点之间',
'会议后的空闲时间限制': '要求在会议结束后至少有 10 分钟的空闲时间(空闲时间必须在她上午 9 点到下午 5 点之间)',
'结束时间限制': '感到疲倦,要求会议在下午4点之前结束',
'星期三午饭时间': '星期三需要在12:30到1:00之间吃午饭',
'清空早上日程': '可以取消每天上午9:00到9:45之间的日程安排,(9:45之前结束的才可以取消)',
'时区差异': '所处的时区,比其他时区要早一个小时',
'时间偏好二四': '喜欢在周二和周四举行简短的会议,并且只在这两天接受新的会议,会议时间不超过1小时',
'30分钟短会议清除': '可以从日程安排中清除任何30分钟或更短的会议',
'10分钟短会议清除': '可以灵活缺席最多10分钟的会议',
'时间偏好二五': '喜欢在周二和周五举行长时间的会议,并且只有在这两天接受新的会议,但会议时间不得超过 30 分钟',
}
if not days_schedules:
return rules, rules_english
def change_schedule(rule:str, days_schedules:dict):
"""
根据规则,修改日程安排
"""
def remove_duration_sche(days_schedules, max_duration):
for key in days_schedules:
new_day_schedule = []
for sche in days_schedules[key][1]:
if sche[1]-sche[0]<=max_duration:
continue
new_day_schedule.append(sche)
days_schedules[key][1] = new_day_schedule
return days_schedules
if rule=='会议前的空闲时间限制':
for key in days_schedules:
for sche in days_schedules[key][1]:
sche[1] += 2
elif rule=='会议后的空闲时间限制':
for key in days_schedules:
for sche in days_schedules[key][1]:
sche[0] -= 2
elif rule=='星期三午饭时间':
days_schedules['三'][1].append([42, 48])
elif rule=='结束时间限制':
for key in days_schedules:
days_schedules[key][1].append([84, 96])
elif rule=='清空早上日程':
for key in days_schedules:
new_day_schedule = []
for sche in days_schedules[key][1]:
if sche[0]<9 and sche[1]<=9:
continue
new_day_schedule.append(sche)
days_schedules[key][1] = new_day_schedule
elif rule=='时区差异':
for key in days_schedules:
days_schedules[key][1].append([84, 96])
elif rule=='30分钟短会议清除':
days_schedules = remove_duration_sche(days_schedules, 6)
elif rule=='时间偏好二四':
days_schedules['一'][1].append([0, 96])
days_schedules['三'][1].append([0, 96])
days_schedules['五'][1].append([0, 96])
elif rule=='10分钟短会议清除':
days_schedules = remove_duration_sche(days_schedules, 2)
elif rule=='时间偏好二五':
days_schedules['一'][1].append([0, 96])
days_schedules['三'][1].append([0, 96])
days_schedules['四'][1].append([0, 96])
return days_schedules
if not choosed_rule:
return days_schedules
days_schedules_copy = copy.deepcopy(days_schedules)
days_schedules_copy = change_schedule(choosed_rule, days_schedules_copy)
return days_schedules_copy
def cal_maxLen_time(self, rules:dict):
"""
根据特殊规则的限制,获取最长会议时间
时间偏好二四:限制一小时以内
时间偏好二五:限制30分钟以内
"""
maxLen_time = 480
for name in rules:
if rules[name]=='时间偏好二四':
maxLen_time = min(maxLen_time, 60)
elif rules[name]=='时间偏好二五':
maxLen_time = min(maxLen_time, 30)
return maxLen_time
def cal_answers(self, schedule_withRule:dict, time_limit:int=480):
"""
根据日程安排表,计算最终答案
限制要求:
1、会议必须在整点或半点开始;
2、有些规则中,存在最长会议时间的限制
@param schedule_withRule: 引入规则限制之后的日程安排,可以直接计算答案
@param time_limit: 最长会议时间限制
@return [最长会议时间, 可选择的选项数]
"""
record = np.zeros((96, 5))
map_dict = {'一':0, '二':1, '三':2, '四':3, '五':4}
for name in schedule_withRule.keys():
days_schedules = schedule_withRule[name]
for key in days_schedules:
map_id = map_dict[key]
for sche in days_schedules[key][1]:
for tmp in range(max(0, sche[0]), min(sche[1], 96)):
record[tmp, map_id] = 1
answer_maxLen = 0
answer_record = []
for col in range(5):
for row in range(0, 96, 6): # 以整点或半点开始查找
if record[row, col]==1:
continue
cur_end = row
length = 0
while(cur_end<96 and record[cur_end, col]==0 and length<time_limit):
cur_end += 1
length += 5
answer_record.append([row, cur_end])
answer_maxLen = max(answer_maxLen, length)
answer_nums = 0
for tmp in answer_record:
if (tmp[1]-tmp[0])*5==answer_maxLen:
answer_nums += 1
return answer_record, answer_nums, answer_maxLen
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Time Sequence任务生成器")
parser.add_argument("--num_of_data", type=int, default=100, help="生成的题目数量")
parser.add_argument("--max_attempts", type=int, default=1000, help="每个题目的最大尝试次数")
parser.add_argument("--n", type=int, default=3, help="人物数量")
parser.add_argument("--rule_prob", type=int, default=50, help="额外规则生成概率")
args = parser.parse_args()
# 创建数据目录
base_data_dir = pathlib.Path(__file__).parent.parent / "data"
if not base_data_dir.exists():
base_data_dir.mkdir(parents=True, exist_ok=True)
# 创建游戏实例
game = TimeSequence()
# 生成游戏数据
game_data_list = game.generate(
num_of_questions=args.num_of_data,
max_attempts=args.max_attempts,
n=args.n,
rule_prob=args.rule_prob
)
print(f"成功生成 {len(game_data_list)} 条游戏数据")
# 创建嵌套目录结构
nested_dir = base_data_dir / f"num_of_data_{args.num_of_data}" / f"n_{args.n}" / f"rule_prob_{args.rule_prob}"
if not nested_dir.exists():
nested_dir.mkdir(parents=True, exist_ok=True)
# 设置输出文件名
output_file = nested_dir / f"time_sequence_{args.num_of_data}.jsonl"
# 将数据保存到文件
try:
with open(output_file, "w", encoding="utf-8") as f:
for game_data in game_data_list:
f.write(json.dumps(game_data.to_json(), ensure_ascii=False) + "\n")
print(f"游戏数据已保存到 {output_file}")
except Exception as e:
print(f"保存数据时出错: {e}")