codefix-env 0.2.1__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.
- codefix_env/__init__.py +80 -0
- codefix_env/cli.py +83 -0
- codefix_env/client.py +194 -0
- codefix_env/env.py +503 -0
- codefix_env/models.py +234 -0
- codefix_env/rewards.py +157 -0
- codefix_env/tasks/__init__.py +75 -0
- codefix_env/tasks/easy.py +244 -0
- codefix_env/tasks/hard.py +430 -0
- codefix_env/tasks/medium.py +342 -0
- codefix_env/utils/__init__.py +33 -0
- codefix_env/utils/metrics.py +165 -0
- codefix_env/utils/reward_model.py +79 -0
- codefix_env/utils/sandbox.py +392 -0
- codefix_env-0.2.1.dist-info/METADATA +663 -0
- codefix_env-0.2.1.dist-info/RECORD +20 -0
- codefix_env-0.2.1.dist-info/WHEEL +5 -0
- codefix_env-0.2.1.dist-info/entry_points.txt +2 -0
- codefix_env-0.2.1.dist-info/licenses/LICENSE +201 -0
- codefix_env-0.2.1.dist-info/top_level.txt +1 -0
codefix_env/models.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeFix-Env Data Models
|
|
3
|
+
=======================
|
|
4
|
+
All Pydantic v2 models for Actions, Observations, State, and Tasks.
|
|
5
|
+
Strongly typed — no `Any`, no raw dicts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field, field_validator
|
|
15
|
+
|
|
16
|
+
# ─────────────────────────────────────────────
|
|
17
|
+
# Enums
|
|
18
|
+
# ─────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ActionType(str, Enum):
|
|
22
|
+
RUN_TESTS = "run_tests"
|
|
23
|
+
EDIT_LINE = "edit_line"
|
|
24
|
+
INSERT_LINE = "insert_line"
|
|
25
|
+
DELETE_LINE = "delete_line"
|
|
26
|
+
GET_HINT = "get_hint"
|
|
27
|
+
SUBMIT_FIX = "submit_fix"
|
|
28
|
+
RESET = "reset"
|
|
29
|
+
VIEW_CODE = "view_code"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Difficulty(str, Enum):
|
|
33
|
+
EASY = "easy"
|
|
34
|
+
MEDIUM = "medium"
|
|
35
|
+
HARD = "hard"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BugCategory(str, Enum):
|
|
39
|
+
SYNTAX = "syntax_error"
|
|
40
|
+
LOGIC = "logic_bug"
|
|
41
|
+
OFF_BY_ONE = "off_by_one"
|
|
42
|
+
TYPE_ERROR = "type_error"
|
|
43
|
+
MISSING_IMPORT = "missing_import"
|
|
44
|
+
WRONG_OPERATOR = "wrong_operator"
|
|
45
|
+
RECURSION_BUG = "recursion_bug"
|
|
46
|
+
ALGORITHM_BUG = "algorithm_bug"
|
|
47
|
+
MULTI_FUNCTION = "multi_function"
|
|
48
|
+
RETURN_BUG = "return_bug"
|
|
49
|
+
INDEX_ERROR = "index_error"
|
|
50
|
+
SCOPE_BUG = "scope_bug"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TerminationReason(str, Enum):
|
|
54
|
+
SOLVED = "solved"
|
|
55
|
+
MAX_STEPS = "max_steps_reached"
|
|
56
|
+
SUBMITTED = "submitted"
|
|
57
|
+
TIMEOUT = "timeout"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ─────────────────────────────────────────────
|
|
61
|
+
# Action
|
|
62
|
+
# ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CodeFixAction(BaseModel):
|
|
66
|
+
"""
|
|
67
|
+
An action taken by the agent in the CodeFix environment.
|
|
68
|
+
|
|
69
|
+
Examples::
|
|
70
|
+
|
|
71
|
+
CodeFixAction(action_type=ActionType.EDIT_LINE, line_number=5, new_content=" return x + 1")
|
|
72
|
+
CodeFixAction(action_type=ActionType.RUN_TESTS)
|
|
73
|
+
CodeFixAction(action_type=ActionType.SUBMIT_FIX)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
action_type: ActionType = Field(..., description="Type of action to perform")
|
|
77
|
+
line_number: int | None = Field(
|
|
78
|
+
None, ge=1, description="Target line (1-indexed, for edit/insert/delete)"
|
|
79
|
+
)
|
|
80
|
+
new_content: str | None = Field(None, description="New line content (for edit/insert)")
|
|
81
|
+
reasoning: str | None = Field(
|
|
82
|
+
None, description="Agent's chain-of-thought (logged, not used by env)"
|
|
83
|
+
)
|
|
84
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
85
|
+
|
|
86
|
+
@field_validator("line_number")
|
|
87
|
+
@classmethod
|
|
88
|
+
def line_required_for_edits(cls, v: int | None, info) -> int | None:
|
|
89
|
+
action = info.data.get("action_type")
|
|
90
|
+
if (
|
|
91
|
+
action in (ActionType.EDIT_LINE, ActionType.INSERT_LINE, ActionType.DELETE_LINE)
|
|
92
|
+
and v is None
|
|
93
|
+
):
|
|
94
|
+
raise ValueError(f"line_number is required for action_type={action}")
|
|
95
|
+
return v
|
|
96
|
+
|
|
97
|
+
@field_validator("new_content")
|
|
98
|
+
@classmethod
|
|
99
|
+
def content_required_for_edits(cls, v: str | None, info) -> str | None:
|
|
100
|
+
action = info.data.get("action_type")
|
|
101
|
+
if action in (ActionType.EDIT_LINE, ActionType.INSERT_LINE) and v is None:
|
|
102
|
+
raise ValueError(f"new_content is required for action_type={action}")
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
model_config = {"use_enum_values": True}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ─────────────────────────────────────────────
|
|
109
|
+
# Observation
|
|
110
|
+
# ─────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestResult(BaseModel):
|
|
114
|
+
"""Result of a single test case."""
|
|
115
|
+
|
|
116
|
+
name: str
|
|
117
|
+
passed: bool
|
|
118
|
+
output: str = ""
|
|
119
|
+
error: str = ""
|
|
120
|
+
runtime_ms: float = 0.0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CodeFixObservation(BaseModel):
|
|
124
|
+
"""
|
|
125
|
+
Full observation returned after every env.step() or env.reset().
|
|
126
|
+
Contains everything the agent needs to make the next decision.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# Code state
|
|
130
|
+
current_code: str = Field(..., description="Current (possibly edited) source code")
|
|
131
|
+
original_code: str = Field(..., description="Original buggy code (never changes)")
|
|
132
|
+
diff: str = Field("", description="Unified diff: original → current")
|
|
133
|
+
|
|
134
|
+
# Test feedback
|
|
135
|
+
test_results: list[TestResult] = Field(default_factory=list)
|
|
136
|
+
test_output: str = Field("", description="Raw stdout/stderr from test runner")
|
|
137
|
+
tests_passed: int = 0
|
|
138
|
+
tests_total: int = 0
|
|
139
|
+
all_tests_pass: bool = False
|
|
140
|
+
|
|
141
|
+
# Scoring
|
|
142
|
+
score_so_far: float = Field(0.0, ge=0.0, le=1.0, description="Cumulative normalised score")
|
|
143
|
+
shaped_reward: float = Field(0.0, description="Dense reward for this step")
|
|
144
|
+
|
|
145
|
+
# Episode metadata
|
|
146
|
+
step_count: int = 0
|
|
147
|
+
max_steps: int = 20
|
|
148
|
+
steps_remaining: int = 20
|
|
149
|
+
done: bool = False
|
|
150
|
+
termination_reason: TerminationReason | None = None
|
|
151
|
+
|
|
152
|
+
# Task info
|
|
153
|
+
task_id: str = ""
|
|
154
|
+
difficulty: Difficulty = Difficulty.MEDIUM
|
|
155
|
+
bug_category: BugCategory = BugCategory.LOGIC
|
|
156
|
+
hint_available: bool = True
|
|
157
|
+
hints_used: int = 0
|
|
158
|
+
|
|
159
|
+
# Agent feedback
|
|
160
|
+
feedback: str = ""
|
|
161
|
+
error_message: str = ""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ─────────────────────────────────────────────
|
|
165
|
+
# Episode State
|
|
166
|
+
# ─────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class CodeFixState(BaseModel):
|
|
170
|
+
"""Internal episode state (returned by env.state())."""
|
|
171
|
+
|
|
172
|
+
episode_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
173
|
+
task_id: str = ""
|
|
174
|
+
step_count: int = 0
|
|
175
|
+
max_steps: int = 20
|
|
176
|
+
total_reward: float = 0.0
|
|
177
|
+
action_history: list[dict] = Field(default_factory=list)
|
|
178
|
+
start_time: datetime = Field(default_factory=datetime.utcnow)
|
|
179
|
+
done: bool = False
|
|
180
|
+
solved: bool = False
|
|
181
|
+
hints_used: int = 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ─────────────────────────────────────────────
|
|
185
|
+
# Step Result (wraps observation)
|
|
186
|
+
# ─────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class StepResult(BaseModel):
|
|
190
|
+
"""Full result returned by env.step()."""
|
|
191
|
+
|
|
192
|
+
observation: CodeFixObservation
|
|
193
|
+
reward: float
|
|
194
|
+
done: bool
|
|
195
|
+
truncated: bool = False
|
|
196
|
+
info: dict = Field(default_factory=dict)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ─────────────────────────────────────────────
|
|
200
|
+
# Task Definition
|
|
201
|
+
# ─────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestCase(BaseModel):
|
|
205
|
+
"""A single test case for a task."""
|
|
206
|
+
|
|
207
|
+
name: str
|
|
208
|
+
code: str = Field(..., description="Test code (function call + assertion)")
|
|
209
|
+
expected: str = ""
|
|
210
|
+
timeout_s: float = 5.0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Task(BaseModel):
|
|
214
|
+
"""A complete debugging task."""
|
|
215
|
+
|
|
216
|
+
id: str
|
|
217
|
+
title: str
|
|
218
|
+
description: str
|
|
219
|
+
buggy_code: str
|
|
220
|
+
solution_code: str
|
|
221
|
+
test_cases: list[TestCase]
|
|
222
|
+
difficulty: Difficulty
|
|
223
|
+
bug_category: BugCategory
|
|
224
|
+
tags: list[str] = Field(default_factory=list)
|
|
225
|
+
hints: list[str] = Field(default_factory=list)
|
|
226
|
+
max_steps: int = 20
|
|
227
|
+
hint_penalty: float = 0.05
|
|
228
|
+
time_penalty_per_step: float = 0.01
|
|
229
|
+
author: str = "codefix-env"
|
|
230
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def num_tests(self) -> int:
|
|
234
|
+
return len(self.test_cases)
|
codefix_env/rewards.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reward Shaping Pipeline
|
|
3
|
+
========================
|
|
4
|
+
High-level reward computation that combines:
|
|
5
|
+
1. Rule-based rewards (always active)
|
|
6
|
+
2. Neural reward model (optional, loaded from checkpoint)
|
|
7
|
+
3. Reward normalisation across an episode
|
|
8
|
+
|
|
9
|
+
Used by the environment server on every step.
|
|
10
|
+
|
|
11
|
+
NOTE: torch and RewardMLP are imported lazily inside __init__, only when
|
|
12
|
+
a neural_model_path is actually provided. This keeps importing
|
|
13
|
+
codefix_env fast (no torch load) for the common case of rule-based-only
|
|
14
|
+
reward, which matters a lot for sandboxed worker processes that re-import
|
|
15
|
+
the whole package on every spawn (see utils/sandbox.py).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from codefix_env.models import ActionType, CodeFixObservation, Task
|
|
25
|
+
from codefix_env.utils.metrics import (
|
|
26
|
+
EpisodeMetrics,
|
|
27
|
+
ScoringConfig,
|
|
28
|
+
compute_diff_score,
|
|
29
|
+
compute_final_score,
|
|
30
|
+
compute_shaped_reward,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RewardPipeline:
|
|
37
|
+
"""
|
|
38
|
+
Combines rule-based reward with an optional neural reward model.
|
|
39
|
+
|
|
40
|
+
Usage::
|
|
41
|
+
|
|
42
|
+
pipeline = RewardPipeline(cfg=ScoringConfig())
|
|
43
|
+
reward = pipeline.step_reward(prev_obs, curr_obs, action_type, task)
|
|
44
|
+
final = pipeline.episode_reward(curr_obs, task)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
cfg: ScoringConfig | None = None,
|
|
50
|
+
neural_model_path: str | None = None,
|
|
51
|
+
neural_weight: float = 0.3,
|
|
52
|
+
):
|
|
53
|
+
self.cfg = cfg if cfg is not None else ScoringConfig()
|
|
54
|
+
self.neural_weight = neural_weight
|
|
55
|
+
self._neural_model = None
|
|
56
|
+
|
|
57
|
+
# Load pre-trained neural reward model if provided.
|
|
58
|
+
# torch + RewardMLP are imported here, not at module top, so the
|
|
59
|
+
# common no-neural-model path never pays torch's import cost.
|
|
60
|
+
if neural_model_path and Path(neural_model_path).exists():
|
|
61
|
+
try:
|
|
62
|
+
import torch
|
|
63
|
+
|
|
64
|
+
from codefix_env.utils.reward_model import RewardMLP
|
|
65
|
+
|
|
66
|
+
self._neural_model = RewardMLP()
|
|
67
|
+
state = torch.load(neural_model_path, map_location="cpu", weights_only=True)
|
|
68
|
+
self._neural_model.load_state_dict(state)
|
|
69
|
+
self._neural_model.eval()
|
|
70
|
+
logger.info("Neural reward model loaded from %s", neural_model_path)
|
|
71
|
+
except Exception as e: # noqa: BLE001
|
|
72
|
+
logger.warning("Failed to load neural reward model: %s", e)
|
|
73
|
+
self._neural_model = None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def uses_neural(self) -> bool:
|
|
77
|
+
return self._neural_model is not None
|
|
78
|
+
|
|
79
|
+
def step_reward(
|
|
80
|
+
self,
|
|
81
|
+
prev_obs: CodeFixObservation,
|
|
82
|
+
curr_obs: CodeFixObservation,
|
|
83
|
+
action_type: ActionType,
|
|
84
|
+
task: Task,
|
|
85
|
+
) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Compute dense reward for a single step.
|
|
88
|
+
Blend of rule-based + optional neural signal.
|
|
89
|
+
"""
|
|
90
|
+
rule_reward = compute_shaped_reward(
|
|
91
|
+
prev_passed=prev_obs.tests_passed,
|
|
92
|
+
curr_passed=curr_obs.tests_passed,
|
|
93
|
+
tests_total=curr_obs.tests_total,
|
|
94
|
+
step_count=curr_obs.step_count,
|
|
95
|
+
hints_used=curr_obs.hints_used,
|
|
96
|
+
action_type=action_type.value if hasattr(action_type, "value") else str(action_type),
|
|
97
|
+
cfg=self.cfg,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if self._neural_model is None:
|
|
101
|
+
return rule_reward
|
|
102
|
+
|
|
103
|
+
# Neural component
|
|
104
|
+
diff_score = compute_diff_score(curr_obs.original_code, curr_obs.current_code)
|
|
105
|
+
action_id = list(ActionType).index(action_type) if action_type in list(ActionType) else 0
|
|
106
|
+
neural_reward = self._neural_model.predict(
|
|
107
|
+
tests_passed_ratio=curr_obs.tests_passed / max(curr_obs.tests_total, 1),
|
|
108
|
+
step_ratio=curr_obs.step_count / max(task.max_steps, 1),
|
|
109
|
+
hints_used=curr_obs.hints_used,
|
|
110
|
+
diff_score=diff_score,
|
|
111
|
+
prev_score=prev_obs.score_so_far,
|
|
112
|
+
action_type_id=action_id,
|
|
113
|
+
code_len_ratio=len(curr_obs.current_code) / max(len(curr_obs.original_code), 1),
|
|
114
|
+
test_count=curr_obs.tests_total,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Weighted blend: mostly rules, neural provides nuance
|
|
118
|
+
blended = (1.0 - self.neural_weight) * rule_reward + self.neural_weight * neural_reward
|
|
119
|
+
return float(blended)
|
|
120
|
+
|
|
121
|
+
def episode_reward(
|
|
122
|
+
self,
|
|
123
|
+
final_obs: CodeFixObservation,
|
|
124
|
+
task: Task,
|
|
125
|
+
) -> float:
|
|
126
|
+
"""Final episode reward on termination."""
|
|
127
|
+
return compute_final_score(
|
|
128
|
+
tests_passed=final_obs.tests_passed,
|
|
129
|
+
tests_total=final_obs.tests_total,
|
|
130
|
+
step_count=final_obs.step_count,
|
|
131
|
+
hints_used=final_obs.hints_used,
|
|
132
|
+
cfg=self.cfg,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def build_metrics(
|
|
136
|
+
self,
|
|
137
|
+
task: Task,
|
|
138
|
+
final_obs: CodeFixObservation,
|
|
139
|
+
total_reward: float,
|
|
140
|
+
) -> EpisodeMetrics:
|
|
141
|
+
return EpisodeMetrics(
|
|
142
|
+
task_id=task.id,
|
|
143
|
+
solved=final_obs.all_tests_pass,
|
|
144
|
+
final_score=final_obs.score_so_far,
|
|
145
|
+
total_steps=final_obs.step_count,
|
|
146
|
+
hints_used=final_obs.hints_used,
|
|
147
|
+
tests_passed=final_obs.tests_passed,
|
|
148
|
+
tests_total=final_obs.tests_total,
|
|
149
|
+
total_reward=total_reward,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Singleton default pipeline (no neural model unless env var is set)
|
|
154
|
+
default_pipeline = RewardPipeline(
|
|
155
|
+
cfg=ScoringConfig(),
|
|
156
|
+
neural_model_path=os.environ.get("CODEFIX_REWARD_MODEL_PATH"),
|
|
157
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Registry — Central access point for all CodeFix tasks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import random
|
|
8
|
+
|
|
9
|
+
from codefix_env.models import Difficulty, Task
|
|
10
|
+
from codefix_env.tasks.easy import EASY_TASKS
|
|
11
|
+
from codefix_env.tasks.hard import HARD_TASKS
|
|
12
|
+
from codefix_env.tasks.medium import MEDIUM_TASKS
|
|
13
|
+
|
|
14
|
+
# ── Master registry ──────────────────────────────────────────────────────────
|
|
15
|
+
ALL_TASKS: dict[str, Task] = {task.id: task for task in EASY_TASKS + MEDIUM_TASKS + HARD_TASKS}
|
|
16
|
+
|
|
17
|
+
TASKS_BY_DIFFICULTY: dict[Difficulty, list[Task]] = {
|
|
18
|
+
Difficulty.EASY: EASY_TASKS,
|
|
19
|
+
Difficulty.MEDIUM: MEDIUM_TASKS,
|
|
20
|
+
Difficulty.HARD: HARD_TASKS,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_task(task_id: str) -> Task:
|
|
25
|
+
"""Load a task by its exact ID."""
|
|
26
|
+
if task_id not in ALL_TASKS:
|
|
27
|
+
raise KeyError(f"Task '{task_id}' not found. Use list_tasks() to see available tasks.")
|
|
28
|
+
return ALL_TASKS[task_id]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def random_task(
|
|
32
|
+
difficulty: Difficulty | None = None,
|
|
33
|
+
exclude: list[str] | None = None,
|
|
34
|
+
) -> Task:
|
|
35
|
+
"""
|
|
36
|
+
Return a random task, optionally filtered by difficulty and
|
|
37
|
+
excluding already-seen task IDs.
|
|
38
|
+
"""
|
|
39
|
+
pool = list(ALL_TASKS.values())
|
|
40
|
+
if difficulty:
|
|
41
|
+
pool = [t for t in pool if t.difficulty == difficulty]
|
|
42
|
+
if exclude:
|
|
43
|
+
pool = [t for t in pool if t.id not in exclude]
|
|
44
|
+
if not pool:
|
|
45
|
+
raise ValueError("No tasks available after applying filters.")
|
|
46
|
+
return random.choice(pool)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_tasks(difficulty: Difficulty | None = None) -> list[Task]:
|
|
50
|
+
"""List all tasks, optionally filtered by difficulty."""
|
|
51
|
+
if difficulty:
|
|
52
|
+
return TASKS_BY_DIFFICULTY.get(difficulty, [])
|
|
53
|
+
return list(ALL_TASKS.values())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def task_count() -> dict[str, int]:
|
|
57
|
+
"""Return count of tasks by difficulty."""
|
|
58
|
+
return {
|
|
59
|
+
"easy": len(EASY_TASKS),
|
|
60
|
+
"medium": len(MEDIUM_TASKS),
|
|
61
|
+
"hard": len(HARD_TASKS),
|
|
62
|
+
"total": len(ALL_TASKS),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"ALL_TASKS",
|
|
68
|
+
"EASY_TASKS",
|
|
69
|
+
"MEDIUM_TASKS",
|
|
70
|
+
"HARD_TASKS",
|
|
71
|
+
"load_task",
|
|
72
|
+
"random_task",
|
|
73
|
+
"list_tasks",
|
|
74
|
+
"task_count",
|
|
75
|
+
]
|