oj-problem-import 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oj_engine/__init__.py +40 -0
- oj_engine/agent/__init__.py +4 -0
- oj_engine/agent/problem_agent.py +364 -0
- oj_engine/cli.py +170 -0
- oj_engine/config/__init__.py +4 -0
- oj_engine/config/settings.py +171 -0
- oj_engine/config_manager.py +185 -0
- oj_engine/config_wizard.py +180 -0
- oj_engine/sandbox.py +361 -0
- oj_engine/services/__init__.py +4 -0
- oj_engine/state.py +68 -0
- oj_engine/tools/__init__.py +22 -0
- oj_engine/tools/sandbox_tools.py +585 -0
- oj_problem_import-0.1.0.dist-info/METADATA +374 -0
- oj_problem_import-0.1.0.dist-info/RECORD +18 -0
- oj_problem_import-0.1.0.dist-info/WHEEL +5 -0
- oj_problem_import-0.1.0.dist-info/entry_points.txt +2 -0
- oj_problem_import-0.1.0.dist-info/top_level.txt +1 -0
oj_engine/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OJ Engine - AI OJ Content Engine
|
|
3
|
+
|
|
4
|
+
采用 ReAct Agent 架构,让 AI 自主决策执行流程。
|
|
5
|
+
"""
|
|
6
|
+
# Agent 模式
|
|
7
|
+
from .agent import ProblemGenerationAgent
|
|
8
|
+
from .tools import (
|
|
9
|
+
execute_code,
|
|
10
|
+
write_code_file,
|
|
11
|
+
read_file_content,
|
|
12
|
+
edit_file_content,
|
|
13
|
+
search_in_file,
|
|
14
|
+
delete_file,
|
|
15
|
+
save_outputs_to_host,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# 底层支持
|
|
19
|
+
from .sandbox import SandboxExecutor
|
|
20
|
+
from .config import Settings, settings, get_settings
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Agent
|
|
24
|
+
"ProblemGenerationAgent",
|
|
25
|
+
|
|
26
|
+
# Tools
|
|
27
|
+
"execute_code",
|
|
28
|
+
"write_code_file",
|
|
29
|
+
"read_file_content",
|
|
30
|
+
"edit_file_content",
|
|
31
|
+
"search_in_file",
|
|
32
|
+
"delete_file",
|
|
33
|
+
"save_outputs_to_host",
|
|
34
|
+
|
|
35
|
+
# Core
|
|
36
|
+
"SandboxExecutor",
|
|
37
|
+
"Settings",
|
|
38
|
+
"settings",
|
|
39
|
+
"get_settings",
|
|
40
|
+
]
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Problem Generation Agent - ReAct Agent for OJ problem content generation
|
|
3
|
+
|
|
4
|
+
使用 ReAct (Reasoning + Acting) 模式,让 AI 自主决策:
|
|
5
|
+
- 何时生成代码
|
|
6
|
+
- 何时执行测试
|
|
7
|
+
- 何时重试或调整策略
|
|
8
|
+
"""
|
|
9
|
+
from langgraph.prebuilt import create_react_agent
|
|
10
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
11
|
+
from ..config import settings
|
|
12
|
+
from ..tools import (
|
|
13
|
+
execute_code,
|
|
14
|
+
write_code_file,
|
|
15
|
+
read_file_content,
|
|
16
|
+
edit_file_content,
|
|
17
|
+
search_in_file,
|
|
18
|
+
delete_file,
|
|
19
|
+
save_outputs_to_host,
|
|
20
|
+
set_global_sandbox_session,
|
|
21
|
+
)
|
|
22
|
+
from ..sandbox import SandboxSession
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ReAct System Prompt
|
|
26
|
+
REACT_SYSTEM_PROMPT = """你是一个专业的 OJ (Online Judge) 题目内容生成专家。
|
|
27
|
+
|
|
28
|
+
你的任务是根据题目描述,生成完整的测试数据包。
|
|
29
|
+
|
|
30
|
+
## 最终产物要求
|
|
31
|
+
|
|
32
|
+
**必须且仅需保留以下文件**:
|
|
33
|
+
1. `solution.py` - 标答代码 (Python)
|
|
34
|
+
2. `generator.py` - 数据生成器 (Python)
|
|
35
|
+
3. `tests/` 目录 - 包含成对的 `.in` 和 `.out` 文件
|
|
36
|
+
- **第1组必须是题目中的样例输入输出** (如果题目提供了样例)
|
|
37
|
+
- 其余测试数据由 generator 生成
|
|
38
|
+
- 例如: `1.in`, `1.out` (样例), `2.in`, `2.out`, ..., `{n}.in`, `{n}.out`
|
|
39
|
+
- **默认生成10组测试数据**,除非题目明确要求其他数量
|
|
40
|
+
- **数据分布**: 30%小数据 + 50%中等数据 + 20%大数据/边缘情况
|
|
41
|
+
|
|
42
|
+
**重要**: 在调用 save_outputs_to_host 之前,必须使用 delete_file 工具删除所有其他临时文件!
|
|
43
|
+
|
|
44
|
+
## 工作流程
|
|
45
|
+
|
|
46
|
+
1. **分析阶段**: 仔细阅读题目要求,确定:
|
|
47
|
+
- 算法类型 (排序、搜索、图论、动态规划等)
|
|
48
|
+
- 数据范围 (n 的最大值、数值范围等)
|
|
49
|
+
- 输入输出格式
|
|
50
|
+
- **提取样例输入输出** (如果题目提供了样例)
|
|
51
|
+
|
|
52
|
+
2. **生成并保存标答**: 编写正确的 solution 代码 (必须使用 Python)
|
|
53
|
+
- 确保算法正确性
|
|
54
|
+
- 考虑边界情况
|
|
55
|
+
- 优化时间复杂度
|
|
56
|
+
- **使用 write_code_file 工具保存到 "solution.py"**
|
|
57
|
+
|
|
58
|
+
3. **生成并保存数据生成器**: 编写 generator 代码 (必须使用 Python)
|
|
59
|
+
- 能够生成符合题目约束的随机数据
|
|
60
|
+
- 支持生成不同规模的数据(小/中/大)
|
|
61
|
+
- 确保生成的数据有效
|
|
62
|
+
- **使用 write_code_file 工具保存到 "generator.py"**
|
|
63
|
+
|
|
64
|
+
4. **执行测试验证**: 使用 execute_code 工具验证标答
|
|
65
|
+
- 构造简单测试用例验证正确性
|
|
66
|
+
- 运行标答验证输出
|
|
67
|
+
- 调整代码直到通过所有测试
|
|
68
|
+
|
|
69
|
+
5. **批量生成测试数据**:
|
|
70
|
+
- 创建 `tests/` 目录
|
|
71
|
+
- **确定测试数据数量**:
|
|
72
|
+
- 如果题目明确要求数量,使用题目要求的数量
|
|
73
|
+
- 否则默认生成10组测试数据
|
|
74
|
+
- **第1组: 使用题目中的样例** (如果有)
|
|
75
|
+
a. 将样例输入保存为 `tests/1.in`
|
|
76
|
+
b. 运行 solution 验证输出是否与样例输出一致
|
|
77
|
+
c. 将样例输出保存为 `tests/1.out`
|
|
78
|
+
- **第2-N组: 由 generator 生成** (N为总数量)
|
|
79
|
+
a. 使用 generator 生成输入,保存为 `tests/{i}.in`
|
|
80
|
+
b. 使用 solution 处理输入,保存输出为 `tests/{i}.out`
|
|
81
|
+
c. 验证输出正确性
|
|
82
|
+
- **确保数据分布**: 30%小数据 + 50%中等数据 + 20%大数据/边缘情况
|
|
83
|
+
- 小数据: 边界值、特殊情况、最小规模
|
|
84
|
+
- 中等数据: 常规规模、典型场景
|
|
85
|
+
- 大数据/边缘: 最大规模、极端情况、最坏情况
|
|
86
|
+
|
|
87
|
+
6. **清理临时文件**: **关键步骤!**
|
|
88
|
+
- 使用 delete_file 删除所有测试过程中创建的临时文件
|
|
89
|
+
- 例如: `test_input.txt`, `test_output.txt`, `temp.py` 等
|
|
90
|
+
- **只保留**: `solution.py`, `generator.py`, `tests/` 目录
|
|
91
|
+
|
|
92
|
+
7. **保存产物**: 调用 save_outputs_to_host 将最终产物复制到主机
|
|
93
|
+
|
|
94
|
+
## 可用工具
|
|
95
|
+
|
|
96
|
+
### 文件写入工具
|
|
97
|
+
- **write_code_file**: 将代码写入沙箱工作目录的文件 (优先使用!)
|
|
98
|
+
用法: write_code_file(filename="solution.py", code="...")
|
|
99
|
+
说明: 先调用此工具保存代码,后续只传文件路径
|
|
100
|
+
|
|
101
|
+
### 文件读取和编辑工具
|
|
102
|
+
- **read_file_content**: 读取沙箱中的文件内容(支持分页)
|
|
103
|
+
用法: read_file_content(filename="solution.py", start_line=1, max_lines=100)
|
|
104
|
+
说明: 用于查看已生成的代码或测试结果,大文件可分批读取
|
|
105
|
+
|
|
106
|
+
- **edit_file_content**: 编辑沙箱中的文件内容
|
|
107
|
+
用法: edit_file_content(filename="solution.py", old_text="...", new_text="...", replace_all=False)
|
|
108
|
+
说明: 精确替换文本,支持替换所有匹配项或仅第一个
|
|
109
|
+
|
|
110
|
+
- **search_in_file**: 在文件中搜索特定字符串
|
|
111
|
+
用法: search_in_file(filename="solution.py", search_text="def main", case_sensitive=True)
|
|
112
|
+
说明: 返回匹配位置和上下文,便于定位代码
|
|
113
|
+
|
|
114
|
+
- **delete_file**: 删除沙箱中的文件
|
|
115
|
+
用法: delete_file(filename="temp.py")
|
|
116
|
+
说明: 清理临时文件或不需要的文件
|
|
117
|
+
|
|
118
|
+
### 基础执行工具
|
|
119
|
+
- **execute_code**: 在沙箱中执行代码文件 (仅支持 Python)
|
|
120
|
+
用法: execute_code(code_file="main.py", input_file="input.txt")
|
|
121
|
+
注意: 只传文件路径,不传代码内容
|
|
122
|
+
|
|
123
|
+
### 产物保存工具
|
|
124
|
+
- **save_outputs_to_host**: 将沙箱中的所有文件复制到主机 outputs 目录
|
|
125
|
+
用法: save_outputs_to_host(problem_title="A+B Problem")
|
|
126
|
+
说明: 在工作完成后调用,自动复制所有生成的文件
|
|
127
|
+
|
|
128
|
+
## 思考原则
|
|
129
|
+
|
|
130
|
+
每次行动前,按照 ReAct 模式思考:
|
|
131
|
+
|
|
132
|
+
**Thought**: 分析当前状态,我需要做什么?
|
|
133
|
+
**Action**: 选择合适的工具
|
|
134
|
+
**Observation**: 查看工具返回的结果
|
|
135
|
+
**Next**: 根据结果决定下一步
|
|
136
|
+
|
|
137
|
+
## 重要提示
|
|
138
|
+
|
|
139
|
+
1. **语言限制**: 所有代码必须使用 Python,不允许使用 C++ 或其他语言
|
|
140
|
+
2. **文件优先**: 先生成代码并用 write_code_file 保存,后续只传文件路径
|
|
141
|
+
3. **避免重复传输代码**: 不要在参数中传递完整代码,这会浪费 token
|
|
142
|
+
4. **检查结果**: 每次执行后验证结果是否合理
|
|
143
|
+
5. **错误处理**: 如果失败,分析原因并调整策略
|
|
144
|
+
6. **样例优先**: **如果题目提供了样例输入输出,第1组测试数据必须是样例**
|
|
145
|
+
7. **数据数量**: **默认生成10组测试数据**,除非题目明确要求其他数量
|
|
146
|
+
8. **数据分布**: 确保测试数据符合 **30%小 + 50%中 + 20%大/边缘** 的分布
|
|
147
|
+
9. **代码质量**: 标答必须正确,generator 必须能生成有效数据
|
|
148
|
+
10. **产物清理**: **最关键!** 在保存前必须删除所有临时文件,只保留 solution.py, generator.py 和 tests/ 目录
|
|
149
|
+
|
|
150
|
+
## 示例流程
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Thought: 我需要先理解题目要求,提取样例输入输出
|
|
154
|
+
Action: 分析题目描述
|
|
155
|
+
Observation: 题目提供了样例: 输入 "3 5", 输出 "8"。题目未明确要求测试数据数量,使用默认10组。
|
|
156
|
+
|
|
157
|
+
Thought: 生成标答代码并保存
|
|
158
|
+
Action: write_code_file(filename="solution.py", code="...")
|
|
159
|
+
Observation: {'success': True, 'filepath': 'solution.py', 'size': 150}
|
|
160
|
+
|
|
161
|
+
Thought: 现在需要生成数据生成器并保存
|
|
162
|
+
Action: write_code_file(filename="generator.py", code="...")
|
|
163
|
+
Observation: {'success': True, 'filepath': 'generator.py', 'size': 200}
|
|
164
|
+
|
|
165
|
+
Thought: 创建 tests 目录并将样例作为第1组测试数据
|
|
166
|
+
Action: write_code_file(filename="tests/1.in", code="3 5")
|
|
167
|
+
Observation: {'success': True, 'filepath': 'tests/1.in', 'size': 4}
|
|
168
|
+
|
|
169
|
+
Thought: 运行标答验证样例输出
|
|
170
|
+
Action: execute_code(code_file="solution.py", input_file="tests/1.in")
|
|
171
|
+
Observation: {'stdout': '8\n', 'exit_code': 0, 'status': 'success'}
|
|
172
|
+
|
|
173
|
+
Thought: 标答输出与样例一致,保存样例输出
|
|
174
|
+
Action: write_code_file(filename="tests/1.out", code="8")
|
|
175
|
+
Observation: {'success': True, 'filepath': 'tests/1.out', 'size': 1}
|
|
176
|
+
|
|
177
|
+
Thought: 现在生成其余9组测试数据 (2-10),确保分布为 3小+5中+2大
|
|
178
|
+
Action: 循环执行 generator 和 solution,将结果保存到 tests/{i}.in 和 tests/{i}.out (i=2..10)
|
|
179
|
+
Observation: 生成了9组测试数据 (tests/2.in, tests/2.out, ..., tests/10.in, tests/10.out)
|
|
180
|
+
|
|
181
|
+
Thought: 检查后9组数据强度分布是否符合 30%小+50%中+20%大 的要求
|
|
182
|
+
Action: 分析输入数据的规模和特征
|
|
183
|
+
Observation: 分布为 small=30%, medium=50%, large=20%, 符合要求!
|
|
184
|
+
|
|
185
|
+
Thought: 清理临时文件,只保留必要文件
|
|
186
|
+
Action: delete_file(filename="test_input.txt")
|
|
187
|
+
Observation: {'success': True, 'message': 'File deleted: test_input.txt'}
|
|
188
|
+
|
|
189
|
+
Thought: 确认沙箱中只有 solution.py, generator.py 和 tests/ 目录
|
|
190
|
+
Action: 检查文件列表
|
|
191
|
+
Observation: 当前文件: solution.py, generator.py, tests/
|
|
192
|
+
|
|
193
|
+
Thought: 所有任务完成,保存产物到主机
|
|
194
|
+
Action: save_outputs_to_host(problem_title="A+B Problem")
|
|
195
|
+
Observation: {'success': True, 'output_path': 'outputs/20260510_123456_A_B_Problem', ...}
|
|
196
|
+
|
|
197
|
+
Thought: 所有任务完成
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
记住:你是专家,要确保生成的内容高质量且可用!
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ProblemGenerationAgent:
|
|
205
|
+
"""
|
|
206
|
+
OJ 题目生成 Agent
|
|
207
|
+
|
|
208
|
+
使用 ReAct 模式自主决策和执行任务
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, max_iterations: int = 20):
|
|
212
|
+
"""
|
|
213
|
+
初始化 Agent
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
max_iterations: 最大迭代次数,防止无限循环
|
|
217
|
+
"""
|
|
218
|
+
# 获取 LLM 客户端
|
|
219
|
+
self.llm = settings.get_llm_client(model_type="generator")
|
|
220
|
+
|
|
221
|
+
# 创建持久化沙箱会话
|
|
222
|
+
self.sandbox_session = SandboxSession()
|
|
223
|
+
print("[Agent] Sandbox session created")
|
|
224
|
+
|
|
225
|
+
# 设置全局沙箱会话(供工具使用)
|
|
226
|
+
set_global_sandbox_session(self.sandbox_session)
|
|
227
|
+
|
|
228
|
+
# 定义工具列表
|
|
229
|
+
self.tools = [
|
|
230
|
+
write_code_file, # 写入代码文件
|
|
231
|
+
read_file_content, # 读取文件内容
|
|
232
|
+
edit_file_content, # 编辑文件内容
|
|
233
|
+
search_in_file, # 搜索文件内容
|
|
234
|
+
delete_file, # 删除文件
|
|
235
|
+
execute_code, # 执行代码
|
|
236
|
+
save_outputs_to_host, # 保存产物到主机
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
# 使用 LangGraph 创建 ReAct Agent (最简单的方式)
|
|
240
|
+
self.agent_executor = create_react_agent(
|
|
241
|
+
self.llm,
|
|
242
|
+
self.tools,
|
|
243
|
+
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
print("[Agent] ProblemGenerationAgent initialized")
|
|
247
|
+
print(f" - Tools: {len(self.tools)}")
|
|
248
|
+
print(f" - Max iterations: {max_iterations}")
|
|
249
|
+
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
"""上下文管理器入口:初始化沙箱会话"""
|
|
252
|
+
self.sandbox_session.initialize()
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
256
|
+
"""上下文管理器出口:清理沙箱会话"""
|
|
257
|
+
self.sandbox_session.cleanup()
|
|
258
|
+
print("[Agent] Sandbox session cleaned up")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def close(self):
|
|
262
|
+
"""
|
|
263
|
+
手动关闭 Agent,清理资源
|
|
264
|
+
|
|
265
|
+
如果不使用上下文管理器,需要手动调用此方法
|
|
266
|
+
"""
|
|
267
|
+
self.sandbox_session.cleanup()
|
|
268
|
+
print("[Agent] Agent closed and resources cleaned up")
|
|
269
|
+
|
|
270
|
+
def generate_problem(self, problem_description: str) -> dict:
|
|
271
|
+
"""
|
|
272
|
+
主入口:根据题目描述生成完整产物
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
problem_description: 题目描述文本
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
dict 包含 Agent 执行的完整结果
|
|
279
|
+
"""
|
|
280
|
+
print("\n[Agent] Starting problem generation...")
|
|
281
|
+
print(f" Problem: {problem_description[:100]}...")
|
|
282
|
+
|
|
283
|
+
# 构建完整的消息 (系统提示 + 任务指令)
|
|
284
|
+
full_prompt = f"""{REACT_SYSTEM_PROMPT}
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
任务:为以下 OJ 题目生成完整的测试数据包
|
|
289
|
+
|
|
290
|
+
题目描述:
|
|
291
|
+
{problem_description}
|
|
292
|
+
|
|
293
|
+
要求:
|
|
294
|
+
1. 生成正确的标答代码 (solution) - **必须使用 Python**
|
|
295
|
+
2. 生成数据生成器 (generator) - **必须使用 Python**
|
|
296
|
+
3. 生成测试数据:
|
|
297
|
+
- **第1组必须是题目中的样例输入输出** (如果题目提供了样例)
|
|
298
|
+
- **测试数据数量**: 如果题目明确要求数量,使用题目要求的数量;否则默认生成10组
|
|
299
|
+
- 其余测试数据由 generator 生成,保存为 tests/{{i}}.in 和 tests/{{i}}.out
|
|
300
|
+
4. 确保所有测试数据都能被标答正确处理
|
|
301
|
+
5. 确保测试数据强度分布符合要求: **30%小 + 50%中 + 20%大/边缘**
|
|
302
|
+
|
|
303
|
+
**重要约束**:
|
|
304
|
+
- 所有代码必须使用 Python 语言
|
|
305
|
+
- 不要生成 C++、Java 或其他语言的代码
|
|
306
|
+
- solution 和 generator 都必须是有效的 Python 代码
|
|
307
|
+
- **最终沙箱中只能有**: solution.py, generator.py, tests/ 目录
|
|
308
|
+
- **必须删除所有临时文件**后再调用 save_outputs_to_host
|
|
309
|
+
|
|
310
|
+
请逐步思考并执行,确保最终产物完整可用。
|
|
311
|
+
|
|
312
|
+
最后,请以 JSON 格式总结你的工作成果:
|
|
313
|
+
{{{{
|
|
314
|
+
"solution_code": "...",
|
|
315
|
+
"generator_code": "...",
|
|
316
|
+
"test_cases_count": 10,
|
|
317
|
+
"data_distribution": {{"small": 3, "medium": 5, "large": 2}},
|
|
318
|
+
"message": "任务完成说明"
|
|
319
|
+
}}}}
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
# 执行 Agent
|
|
323
|
+
try:
|
|
324
|
+
result = self.agent_executor.invoke({
|
|
325
|
+
"messages": [{"role": "user", "content": full_prompt}]
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
print("\n[Agent] Task completed!")
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"\n[Agent] Error: {str(e)}")
|
|
333
|
+
raise
|
|
334
|
+
|
|
335
|
+
def generate_problem_with_retry(self, problem_description: str,
|
|
336
|
+
max_retries: int = 2) -> dict:
|
|
337
|
+
"""
|
|
338
|
+
带重试机制的问题生成
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
problem_description: 题目描述
|
|
342
|
+
max_retries: 最大重试次数
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Agent 执行结果
|
|
346
|
+
"""
|
|
347
|
+
last_error = None
|
|
348
|
+
|
|
349
|
+
for attempt in range(1, max_retries + 1):
|
|
350
|
+
try:
|
|
351
|
+
print(f"\n[Agent] Attempt {attempt}/{max_retries}")
|
|
352
|
+
result = self.generate_problem(problem_description)
|
|
353
|
+
|
|
354
|
+
# 检查结果是否包含必要信息
|
|
355
|
+
if "output" in result:
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
last_error = e
|
|
360
|
+
print(f"[Agent] Attempt {attempt} failed: {str(e)}")
|
|
361
|
+
if attempt < max_retries:
|
|
362
|
+
print("[Agent] Retrying...")
|
|
363
|
+
|
|
364
|
+
raise Exception(f"All {max_retries} attempts failed. Last error: {last_error}")
|
oj_engine/cli.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OJ Engine CLI - 命令行工具
|
|
3
|
+
|
|
4
|
+
提供便捷的命令行接口来生成 OJ 题目测试数据包。
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import click
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from oj_engine.agent import ProblemGenerationAgent
|
|
10
|
+
from oj_engine.config_manager import is_configured, load_config, mask_api_key, get_config_path
|
|
11
|
+
from oj_engine.config_wizard import run_config_wizard
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def main():
|
|
16
|
+
"""OJ Engine - AI OJ 题目生成工具
|
|
17
|
+
|
|
18
|
+
使用 AI 自动生成 OJ 题目的完整测试数据包,包括标答代码、
|
|
19
|
+
数据生成器和多组测试数据。
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@main.command()
|
|
25
|
+
@click.option('--file', '-f', 'file_path', type=click.Path(exists=True),
|
|
26
|
+
help='题目描述文件路径(UTF-8 编码)')
|
|
27
|
+
@click.option('--description', '-d', type=str,
|
|
28
|
+
help='题目描述文本(直接传入)')
|
|
29
|
+
@click.option('--max-iterations', '-m', default=20, type=int,
|
|
30
|
+
help='Agent 最大迭代次数(默认: 20)')
|
|
31
|
+
@click.option('--output-dir', '-o', default='outputs', type=str,
|
|
32
|
+
help='输出目录(默认: outputs)')
|
|
33
|
+
def generate(file_path, description, max_iterations, output_dir):
|
|
34
|
+
"""生成 OJ 题目测试数据包
|
|
35
|
+
|
|
36
|
+
根据题目描述自动生成:
|
|
37
|
+
- 标答代码 (solution.py)
|
|
38
|
+
- 数据生成器 (generator.py)
|
|
39
|
+
- 10组测试数据 (tests/ 目录)
|
|
40
|
+
|
|
41
|
+
示例:
|
|
42
|
+
# 从文件读取题目描述
|
|
43
|
+
oj-engine generate -f problem.txt
|
|
44
|
+
|
|
45
|
+
# 直接传入题目描述
|
|
46
|
+
oj-engine generate -d "A+B Problem..."
|
|
47
|
+
|
|
48
|
+
# 自定义参数
|
|
49
|
+
oj-engine generate -f problem.txt -m 30 -o ./results
|
|
50
|
+
"""
|
|
51
|
+
# 检查配置
|
|
52
|
+
if not is_configured():
|
|
53
|
+
click.echo("\n⚠ 检测到未配置,启动配置向导...\n")
|
|
54
|
+
success = run_config_wizard()
|
|
55
|
+
if not success:
|
|
56
|
+
click.echo("\n✗ 配置失败,无法继续执行", err=True)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
click.echo("\n配置完成!继续执行任务...\n")
|
|
59
|
+
|
|
60
|
+
# 验证参数
|
|
61
|
+
if not file_path and not description:
|
|
62
|
+
click.echo("错误: 必须提供 --file 或 --description 参数", err=True)
|
|
63
|
+
click.echo("使用 'oj-engine generate --help' 查看帮助", err=True)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
# 读取题目描述
|
|
67
|
+
if file_path:
|
|
68
|
+
try:
|
|
69
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
70
|
+
problem_description = f.read()
|
|
71
|
+
click.echo(f"✓ 已从文件读取题目描述: {file_path}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
click.echo(f"错误: 无法读取文件 {file_path}: {e}", err=True)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
else:
|
|
76
|
+
problem_description = description
|
|
77
|
+
|
|
78
|
+
# 验证题目描述不为空
|
|
79
|
+
if not problem_description or not problem_description.strip():
|
|
80
|
+
click.echo("错误: 题目描述不能为空", err=True)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
# 显示配置信息
|
|
84
|
+
click.echo("\n" + "=" * 80)
|
|
85
|
+
click.echo("OJ Engine - 题目生成")
|
|
86
|
+
click.echo("=" * 80)
|
|
87
|
+
click.echo(f"\n配置:")
|
|
88
|
+
click.echo(f" - 最大迭代次数: {max_iterations}")
|
|
89
|
+
click.echo(f" - 输出目录: {output_dir}")
|
|
90
|
+
click.echo(f"\n题目预览:")
|
|
91
|
+
preview = problem_description[:200].strip()
|
|
92
|
+
click.echo(f" {preview}...")
|
|
93
|
+
click.echo("-" * 80)
|
|
94
|
+
|
|
95
|
+
# 执行 Agent
|
|
96
|
+
try:
|
|
97
|
+
click.echo("\n开始生成题目...\n")
|
|
98
|
+
|
|
99
|
+
with ProblemGenerationAgent(max_iterations=max_iterations) as agent:
|
|
100
|
+
result = agent.generate_problem(problem_description)
|
|
101
|
+
|
|
102
|
+
# 显示结果
|
|
103
|
+
click.echo("\n" + "=" * 80)
|
|
104
|
+
click.echo("生成完成!")
|
|
105
|
+
click.echo("=" * 80)
|
|
106
|
+
|
|
107
|
+
# 检查是否有输出
|
|
108
|
+
if "output" in result:
|
|
109
|
+
output_preview = result["output"][:500]
|
|
110
|
+
click.echo(f"\n输出预览(前500字符):")
|
|
111
|
+
click.echo(output_preview)
|
|
112
|
+
click.echo("...")
|
|
113
|
+
|
|
114
|
+
# 显示产物保存路径
|
|
115
|
+
outputs_dir = Path(output_dir)
|
|
116
|
+
if outputs_dir.exists():
|
|
117
|
+
click.echo(f"\n✓ 产物已保存到: {outputs_dir.absolute()}")
|
|
118
|
+
|
|
119
|
+
# 列出最新的输出
|
|
120
|
+
output_dirs = sorted(outputs_dir.iterdir(),
|
|
121
|
+
key=lambda x: x.stat().st_mtime,
|
|
122
|
+
reverse=True)
|
|
123
|
+
if output_dirs:
|
|
124
|
+
latest = output_dirs[0]
|
|
125
|
+
click.echo(f"\n最新输出:")
|
|
126
|
+
click.echo(f" {latest.name}")
|
|
127
|
+
click.echo(f"\n文件列表:")
|
|
128
|
+
for item in sorted(latest.rglob("*")):
|
|
129
|
+
if item.is_file():
|
|
130
|
+
rel_path = item.relative_to(latest)
|
|
131
|
+
click.echo(f" {rel_path}")
|
|
132
|
+
else:
|
|
133
|
+
click.echo(f"\n⚠ 未找到输出目录: {outputs_dir}")
|
|
134
|
+
|
|
135
|
+
click.echo("\n" + "=" * 80)
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
click.echo(f"\n✗ 错误: {str(e)}", err=True)
|
|
139
|
+
import traceback
|
|
140
|
+
traceback.print_exc()
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@main.command()
|
|
145
|
+
def configure():
|
|
146
|
+
"""重新配置 OJ Engine"""
|
|
147
|
+
click.echo("启动配置向导...")
|
|
148
|
+
success = run_config_wizard()
|
|
149
|
+
if not success:
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@main.command()
|
|
154
|
+
def show_config():
|
|
155
|
+
"""显示当前配置(隐藏敏感信息)"""
|
|
156
|
+
config = load_config()
|
|
157
|
+
if config:
|
|
158
|
+
click.echo("当前配置:")
|
|
159
|
+
click.echo(f" LLM 提供商: {config['llm']['provider']}")
|
|
160
|
+
click.echo(f" 模型: {config['llm']['model']}")
|
|
161
|
+
click.echo(f" API Key: {mask_api_key(config['llm']['api_key'])}")
|
|
162
|
+
if config['llm'].get('base_url'):
|
|
163
|
+
click.echo(f" Base URL: {config['llm']['base_url']}")
|
|
164
|
+
click.echo(f" 配置文件: {get_config_path()}")
|
|
165
|
+
else:
|
|
166
|
+
click.echo("未配置,请运行 'oj-engine configure'")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|