"""
AIME数学题目生成器
使用HelloAgents框架生成AIME风格的数学题目
"""
import json
import os
import time
import random
from typing import List, Dict, Any, Optional
from datetime import datetime
from tqdm import tqdm
from hello_agents import SimpleAgent
from hello_agents import HelloAgentsLLM
from datasets import load_dataset
class AIMEGenerator:
"""AIME题目生成器"""
GENERATION_PROMPT = """You are a professional mathematics competition problem designer, skilled in creating AIME (American Invitational Mathematics Examination) style problems.
AIME Problem Characteristics:
1. Answer: An integer between 0 and 999
2. Topics: Algebra, Geometry, Number Theory, Combinatorics, Probability, etc.
3. Style: Requires multi-step reasoning, but no advanced theory
4. Difficulty: Medium to hard (similar to AIME problems 6-9)
Please generate an AIME-style mathematics problem, including:
1. Problem statement (clear and complete)
2. Answer (an integer between 0 and 999)
3. Detailed solution (including all reasoning steps)
4. Topic classification (Algebra/Geometry/Number Theory/Combinatorics/Probability)
Please output in the following JSON format, avoid using special escape characters in JSON:
```json
{
"problem": "Problem statement in English",
"answer": 123,
"solution": "Detailed solution steps in English",
"topic": "Algebra"
}
```
"""
def __init__(
self,
llm: HelloAgentsLLM = None,
delay_seconds: float = 1.0,
use_reference_examples: bool = True,
reference_dataset: str = "TianHongZXY/aime-1983-2025"
):
"""
初始化生成器
Args:
llm: LLM实例(可选)
delay_seconds: 每次生成之间的延迟(秒),避免API速率限制
use_reference_examples: 是否使用真题作为参考样例
reference_dataset: 参考数据集名称,默认使用TianHongZXY/aime-1983-2025(900+道题)
"""
if llm is None:
self.llm = HelloAgentsLLM()
else:
self.llm = llm
self.agent = SimpleAgent(
name="AIME Generator",
llm=self.llm,
system_prompt="你是一位专业的数学竞赛题目设计专家。"
)
self.delay_seconds = delay_seconds
self.use_reference_examples = use_reference_examples
self.reference_examples = []
if use_reference_examples:
try:
print(f"📚 加载AIME真题数据集: {reference_dataset}")
try:
dataset = load_dataset(reference_dataset, split="train")
except:
dataset = load_dataset(reference_dataset, split="test")
self.reference_examples = list(dataset)
print(f" ✓ 已加载 {len(self.reference_examples)} 道参考题目")
year_counts = {}
for item in self.reference_examples:
year = item.get('year')
if year:
year_counts[year] = year_counts.get(year, 0) + 1
if year_counts:
year_range = f"{min(year_counts.keys())}-{max(year_counts.keys())}"
print(f" ℹ️ 年份范围: {year_range}")
except Exception as e:
print(f" ⚠️ 加载参考样例失败: {e}")
print(f" ℹ️ 将使用默认提示词生成")
self.use_reference_examples = False
def generate_single(self, max_retries: int = 3) -> Dict[str, Any]:
"""
生成单个题目
Args:
max_retries: 最大重试次数
Returns:
题目数据
"""
prompt = self._build_prompt()
for attempt in range(max_retries):
try:
response = self.agent.run(prompt)
return self._parse_response(response)
except Exception as e:
if attempt < max_retries - 1:
tqdm.write(f"⚠️ 生成失败(尝试 {attempt + 1}/{max_retries}),{self.delay_seconds}秒后重试...")
time.sleep(self.delay_seconds)
else:
tqdm.write(f"❌ 生成失败,已达最大重试次数: {e}")
return self._get_default_problem()
def _build_prompt(self) -> str:
"""构建生成提示词"""
if not self.use_reference_examples or not self.reference_examples:
return self.GENERATION_PROMPT
example = random.choice(self.reference_examples)
example_problem = example.get('problem', 'Example problem')
example_answer = example.get('answer', 0)
prompt = f"""You are a professional mathematics competition problem designer, skilled in creating AIME (American Invitational Mathematics Examination) style problems.
【Reference Example】(For style reference only, please generate a completely different problem)
Problem: {example_problem}
Answer: {example_answer}
AIME Problem Characteristics:
1. Answer: An integer between 0 and 999
2. Topics: Algebra, Geometry, Number Theory, Combinatorics, Probability, etc.
3. Style: Requires multi-step reasoning, but no advanced theory
4. Difficulty: Medium to hard (similar to AIME problems 6-9)
Please generate a **completely different** AIME-style mathematics problem, including:
1. Problem statement (clear and complete, different from the reference)
2. Answer (an integer between 0 and 999, different from the reference)
3. Detailed solution (including all reasoning steps)
4. Topic classification (Algebra/Geometry/Number Theory/Combinatorics/Probability)
Please output in the following JSON format, avoid using special escape characters in JSON:
```json
{{
"problem": "Problem statement in English",
"answer": 123,
"solution": "Detailed solution steps in English",
"topic": "Algebra"
}}
```
Important Notes:
- **Must generate a completely different problem from the reference**
- You can reference the style, but do not copy the content
- Ensure the problem is creative and original
"""
return prompt
def _parse_response(self, response: str) -> Dict[str, Any]:
"""解析LLM响应(支持LaTeX数学公式)"""
import re
if "```json" in response:
json_str = response.split("```json")[1].split("```")[0].strip()
elif "```" in response:
json_str = response.split("```")[1].split("```")[0].strip()
else:
json_str = response.strip()
try:
problem_data = json.loads(json_str)
except json.JSONDecodeError as e:
fixed_json_str = re.sub(r'(?<!\\)\\(?!["\\/bfnrtu])', r'\\\\', json_str)
try:
problem_data = json.loads(fixed_json_str)
except json.JSONDecodeError:
print(f"❌ JSON解析失败:")
print(f"原始响应: {response[:500]}...")
print(f"提取的JSON: {json_str[:500]}...")
raise
if "problem" not in problem_data or "answer" not in problem_data:
raise ValueError("缺少必需字段: problem 或 answer")
answer = int(problem_data.get("answer", 0))
if not (0 <= answer <= 999):
print(f"⚠️ 答案超出范围: {answer},调整为0-999范围内")
answer = max(0, min(999, answer))
problem_data["answer"] = answer
problem_data.setdefault("solution", "No solution provided")
problem_data.setdefault("topic", "Uncategorized")
return problem_data
def _get_default_problem(self) -> Dict[str, Any]:
"""获取默认题目(生成失败时使用)"""
return {
"problem": "生成失败,请重新生成",
"answer": 0,
"solution": "N/A",
"topic": "未知"
}
def generate_batch(
self,
num_problems: int = 30,
checkpoint_path: str = None
) -> List[Dict[str, Any]]:
"""
批量生成题目
Args:
num_problems: 生成题目数量
checkpoint_path: 检查点文件路径(用于保存进度)
Returns:
题目列表
"""
print(f"\n🎯 开始生成AIME题目")
print(f" 目标数量: {num_problems}")
print(f" 生成模型: {self.llm.model}")
print(f" 延迟设置: {self.delay_seconds}秒/题")
problems = []
start_index = 0
if checkpoint_path and os.path.exists(checkpoint_path):
print(f"\n📂 发现检查点文件,尝试恢复...")
try:
with open(checkpoint_path, 'r', encoding='utf-8') as f:
problems = json.load(f)
start_index = len(problems)
print(f" ✓ 已恢复 {start_index} 个题目,从第 {start_index + 1} 个继续")
except Exception as e:
print(f" ⚠️ 恢复失败: {e},从头开始")
problems = []
start_index = 0
with tqdm(total=num_problems, initial=start_index, desc="生成AIME题目", unit="题") as pbar:
last_call_time = 0
for i in range(start_index, num_problems):
if last_call_time > 0:
elapsed = time.time() - last_call_time
if elapsed < self.delay_seconds:
wait_time = self.delay_seconds - elapsed
tqdm.write(f"⏳ 等待 {wait_time:.1f} 秒以避免速率限制...")
time.sleep(wait_time)
start_time = time.time()
problem = self.generate_single()
problem["id"] = f"gen_aime_{i + 1}"
problem["generated_at"] = datetime.now().isoformat()
last_call_time = time.time()
generation_time = last_call_time - start_time
problems.append(problem)
pbar.set_postfix({
"主题": problem.get('topic', 'N/A'),
"答案": problem.get('answer', 'N/A'),
"耗时": f"{generation_time:.1f}s"
})
pbar.update(1)
if checkpoint_path:
try:
with open(checkpoint_path, 'w', encoding='utf-8') as f:
json.dump(problems, f, ensure_ascii=False, indent=2)
except Exception as e:
tqdm.write(f"⚠️ 保存检查点失败: {e}")
print(f"\n✅ 生成完成!共 {len(problems)} 个题目")
return problems
def save_problems(
self,
problems: List[Dict[str, Any]],
output_path: str
):
"""保存题目到文件"""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(problems, f, ensure_ascii=False, indent=2)
print(f"\n💾 题目已保存: {output_path}")
def generate_and_save(
self,
num_problems: int = 30,
output_dir: str = "data_generation/generated_data"
) -> str:
"""生成并保存题目"""
os.makedirs(output_dir, exist_ok=True)
for file in os.listdir(output_dir):
if file.startswith("checkpoint_") and file.endswith(".json"):
old_checkpoint = os.path.join(output_dir, file)
try:
os.remove(old_checkpoint)
print(f"🗑️ 已删除旧检查点文件: {file}")
except Exception as e:
print(f"⚠️ 删除旧检查点失败: {e}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
checkpoint_path = os.path.join(output_dir, f"checkpoint_{timestamp}.json")
problems = self.generate_batch(num_problems, checkpoint_path=checkpoint_path)
output_path = os.path.join(output_dir, f"aime_generated_{timestamp}.json")
self.save_problems(problems, output_path)
self._generate_statistics_report(problems, output_dir, timestamp)
if os.path.exists(checkpoint_path):
try:
os.remove(checkpoint_path)
print(f"\n🗑️ 已删除检查点文件")
except Exception as e:
print(f"\n⚠️ 删除检查点文件失败: {e}")
return output_path
def _generate_statistics_report(
self,
problems: List[Dict[str, Any]],
output_dir: str,
timestamp: str
):
"""生成统计报告"""
topics = {}
answers = []
for problem in problems:
topic = problem.get("topic", "未知")
topics[topic] = topics.get(topic, 0) + 1
if "answer" in problem:
answers.append(problem["answer"])
report = f"""# AIME题目生成统计报告
## 基本信息
- **生成时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
- **题目数量**: {len(problems)}
## 主题分布
| 主题 | 数量 | 占比 |
|------|------|------|
"""
for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True):
percentage = count / len(problems) * 100
report += f"| {topic} | {count} | {percentage:.1f}% |\n"
if answers:
report += f"""
## 答案分析
- **平均答案**: {sum(answers) / len(answers):.2f}
- **最小答案**: {min(answers)}
- **最大答案**: {max(answers)}
- **答案范围**: {min(answers)}-{max(answers)}
"""
report += f"""
## 题目列表
| ID | 主题 | 答案 |
|-----|------|------|
"""
for problem in problems[:10]:
report += f"| {problem.get('id', 'N/A')} | {problem.get('topic', 'N/A')} | {problem.get('answer', 'N/A')} |\n"
if len(problems) > 10:
report += f"\n*(仅显示前10个题目,完整列表请查看JSON文件)*\n"
report += f"""
---
*报告生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}*
"""
report_path = os.path.join(output_dir, f"generation_report_{timestamp}.md")
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"📊 统计报告已保存: {report_path}")
if __name__ == "__main__":
generator = AIMEGenerator()
output_path = generator.generate_and_save(num_problems=30)
print(f"\n✅ 完成!生成的题目保存在: {output_path}")