@rookiestar/eng-lang-tutor 1.0.1
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.
- package/.claude/settings.local.json +22 -0
- package/.gitignore +32 -0
- package/CHANGELOG.md +37 -0
- package/CLAUDE.md +275 -0
- package/README.md +369 -0
- package/SKILL.md +613 -0
- package/bin/eng-lang-tutor.js +177 -0
- package/docs/OPENCLAW_DEPLOYMENT.md +241 -0
- package/examples/sample_keypoint_a1.json +112 -0
- package/examples/sample_keypoint_a2.json +124 -0
- package/examples/sample_keypoint_b1.json +135 -0
- package/examples/sample_keypoint_b2.json +137 -0
- package/examples/sample_keypoint_c1.json +134 -0
- package/examples/sample_keypoint_c2.json +141 -0
- package/examples/sample_quiz_a1.json +94 -0
- package/examples/sample_quiz_a2.json +94 -0
- package/examples/sample_quiz_b1.json +92 -0
- package/examples/sample_quiz_b2.json +94 -0
- package/examples/sample_quiz_c1.json +94 -0
- package/examples/sample_quiz_c2.json +104 -0
- package/package.json +41 -0
- package/references/resources.md +292 -0
- package/requirements.txt +16 -0
- package/scripts/__init__.py +28 -0
- package/scripts/audio/__init__.py +23 -0
- package/scripts/audio/composer.py +367 -0
- package/scripts/audio/converter.py +331 -0
- package/scripts/audio/feishu_voice.py +404 -0
- package/scripts/audio/tts/__init__.py +30 -0
- package/scripts/audio/tts/base.py +166 -0
- package/scripts/audio/tts/manager.py +306 -0
- package/scripts/audio/tts/providers/__init__.py +12 -0
- package/scripts/audio/tts/providers/edge.py +111 -0
- package/scripts/audio/tts/providers/xunfei.py +205 -0
- package/scripts/audio/utils.py +63 -0
- package/scripts/cli/__init__.py +7 -0
- package/scripts/cli/cli.py +229 -0
- package/scripts/cli/command_parser.py +336 -0
- package/scripts/core/__init__.py +30 -0
- package/scripts/core/constants.py +125 -0
- package/scripts/core/error_notebook.py +308 -0
- package/scripts/core/gamification.py +405 -0
- package/scripts/core/scorer.py +295 -0
- package/scripts/core/state_manager.py +814 -0
- package/scripts/eng-lang-tutor +16 -0
- package/scripts/scheduling/__init__.py +6 -0
- package/scripts/scheduling/cron_push.py +229 -0
- package/scripts/utils/__init__.py +12 -0
- package/scripts/utils/dedup.py +331 -0
- package/scripts/utils/helpers.py +82 -0
- package/templates/keypoint_schema.json +420 -0
- package/templates/prompt_templates.md +73 -0
- package/templates/prompts/display_guide.md +106 -0
- package/templates/prompts/initialization.md +350 -0
- package/templates/prompts/keypoint_generation.md +272 -0
- package/templates/prompts/output_rules.md +106 -0
- package/templates/prompts/quiz_generation.md +190 -0
- package/templates/prompts/responses.md +339 -0
- package/templates/prompts/shared_enums.md +252 -0
- package/templates/quiz_schema.json +214 -0
- package/templates/state_schema.json +277 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared constants for eng-lang-tutor.
|
|
4
|
+
|
|
5
|
+
This module contains constants used across multiple scripts to avoid duplication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# =============================================================================
|
|
9
|
+
# GAME MECHANICS - Activity Levels & Progression
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
12
|
+
# Level thresholds (XP needed for each level)
|
|
13
|
+
# Note: Level is Activity Level (活跃等级), measuring engagement depth,
|
|
14
|
+
# NOT language ability (which is CEFR level A1-C2)
|
|
15
|
+
LEVEL_THRESHOLDS = [
|
|
16
|
+
0, 50, 100, 200, 350, # 1-5 Starter (启程者)
|
|
17
|
+
550, 800, 1100, 1500, 2000, # 6-10 Traveler (行路人)
|
|
18
|
+
2600, 3300, 4100, 5000, 6000, # 11-15 Explorer (探索者)
|
|
19
|
+
7200, 8500, 10000, 12000, 15000 # 16-20 Pioneer (开拓者)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Level range to name mapping
|
|
23
|
+
LEVEL_NAMES = {
|
|
24
|
+
(1, 5): "Starter", # 启程者
|
|
25
|
+
(6, 10): "Traveler", # 行路人
|
|
26
|
+
(11, 15): "Explorer", # 探索者
|
|
27
|
+
(16, 20): "Pioneer" # 开拓者
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Streak bonus configuration
|
|
31
|
+
STREAK_BONUS_PER_DAY = 0.05 # 5% bonus per day
|
|
32
|
+
STREAK_BONUS_CAP = 2.0 # Maximum 2x multiplier
|
|
33
|
+
|
|
34
|
+
# Gem economy
|
|
35
|
+
STREAK_FREEZE_COST = 50 # gems to buy one streak freeze
|
|
36
|
+
HINT_COST = 10 # gems to buy a quiz hint
|
|
37
|
+
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# QUIZ XP VALUES
|
|
40
|
+
# =============================================================================
|
|
41
|
+
|
|
42
|
+
QUIZ_XP = {
|
|
43
|
+
"multiple_choice": 10,
|
|
44
|
+
"chinglish_fix": 15,
|
|
45
|
+
"fill_blank": 12,
|
|
46
|
+
"dialogue_completion": 15
|
|
47
|
+
}
|
|
48
|
+
QUIZ_PASS_THRESHOLD = 2 # 2/3 correct to pass
|
|
49
|
+
QUIZ_QUESTIONS_PER_DAY = 3
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# ERROR NOTEBOOK
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
ERROR_ARCHIVE_WRONG_THRESHOLD = 3 # Archive if wrong_count >= 3
|
|
56
|
+
ERROR_ARCHIVE_DAYS_THRESHOLD = 30 # Archive if over 30 days old
|
|
57
|
+
ERROR_NOTEBOOK_MAX = 100 # Maximum errors in notebook
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# AUDIO PROCESSING
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
AUDIO_TTS_TIMEOUT = 30 # seconds, for single TTS synthesis
|
|
64
|
+
AUDIO_CONCAT_TIMEOUT = 120 # seconds, for concatenating segments
|
|
65
|
+
AUDIO_CONVERSION_TIMEOUT = 60 # seconds, for format conversion
|
|
66
|
+
|
|
67
|
+
# Audio silence durations
|
|
68
|
+
AUDIO_SILENCE_LEAD_IN = 1.0 # seconds, after lead-in phrase
|
|
69
|
+
AUDIO_SILENCE_SECTION = 2.0 # seconds, between sections
|
|
70
|
+
AUDIO_SILENCE_DIALOGUE = 0.5 # seconds, between dialogue lines
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# DEDUPLICATION
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
DEDUP_DEFAULT_DAYS = 14 # Default days for content deduplication
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# HELPER FUNCTIONS
|
|
81
|
+
# =============================================================================
|
|
82
|
+
|
|
83
|
+
def get_level_name(level: int) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Get the journey stage name for a level.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
level: Activity level (1-20)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Stage name (Starter/Traveler/Explorer/Pioneer)
|
|
92
|
+
"""
|
|
93
|
+
for (min_level, max_level), name in LEVEL_NAMES.items():
|
|
94
|
+
if min_level <= level <= max_level:
|
|
95
|
+
return name
|
|
96
|
+
return "Unknown"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def calculate_level(xp: int) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Calculate level from total XP.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
xp: Total experience points
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Level (1-20)
|
|
108
|
+
"""
|
|
109
|
+
for i in range(len(LEVEL_THRESHOLDS) - 1, -1, -1):
|
|
110
|
+
if xp >= LEVEL_THRESHOLDS[i]:
|
|
111
|
+
return i + 1
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_streak_multiplier(streak: int) -> float:
|
|
116
|
+
"""
|
|
117
|
+
Calculate XP multiplier based on streak.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
streak: Number of consecutive days
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Multiplier (1.0 - 2.0)
|
|
124
|
+
"""
|
|
125
|
+
return min(1.0 + (streak * STREAK_BONUS_PER_DAY), STREAK_BONUS_CAP)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Error Notebook Manager - 错题本管理
|
|
4
|
+
|
|
5
|
+
管理用户的错题本功能,包括:
|
|
6
|
+
- 添加错题
|
|
7
|
+
- 分页查询
|
|
8
|
+
- 统计信息
|
|
9
|
+
- 复习标记
|
|
10
|
+
- 自动归档
|
|
11
|
+
|
|
12
|
+
从 state_manager.py 提取,遵循单一职责原则。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Dict, Any, List
|
|
16
|
+
from datetime import datetime, date
|
|
17
|
+
from collections import Counter
|
|
18
|
+
|
|
19
|
+
from .constants import (
|
|
20
|
+
ERROR_ARCHIVE_WRONG_THRESHOLD,
|
|
21
|
+
ERROR_ARCHIVE_DAYS_THRESHOLD,
|
|
22
|
+
ERROR_NOTEBOOK_MAX
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ErrorNotebookManager:
|
|
27
|
+
"""错题本管理器"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, state_manager=None):
|
|
30
|
+
"""
|
|
31
|
+
初始化错题本管理器
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
state_manager: StateManager 实例,用于事件日志
|
|
35
|
+
"""
|
|
36
|
+
self.state_manager = state_manager
|
|
37
|
+
|
|
38
|
+
def add_to_error_notebook(self, state: Dict[str, Any],
|
|
39
|
+
error: Dict[str, Any]) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Add an error to the error notebook.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
state: Current state
|
|
45
|
+
error: Error entry with date, question, user_answer, correct_answer, explanation
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Updated state
|
|
49
|
+
"""
|
|
50
|
+
error['date'] = date.today().isoformat()
|
|
51
|
+
error['reviewed'] = False
|
|
52
|
+
error['wrong_count'] = error.get('wrong_count', 1)
|
|
53
|
+
# keypoint_date and question_type are optional
|
|
54
|
+
|
|
55
|
+
errors = state.get('error_notebook', [])
|
|
56
|
+
errors.append(error)
|
|
57
|
+
|
|
58
|
+
# Auto-archive oldest if over size limit
|
|
59
|
+
if len(errors) > ERROR_NOTEBOOK_MAX:
|
|
60
|
+
# Sort by date to find oldest
|
|
61
|
+
errors.sort(key=lambda x: x.get('date', ''))
|
|
62
|
+
# Archive the oldest
|
|
63
|
+
oldest = errors.pop(0)
|
|
64
|
+
archive = state.get('error_archive', [])
|
|
65
|
+
oldest['archived_at'] = date.today().isoformat()
|
|
66
|
+
oldest['archived_reason'] = 'notebook_full'
|
|
67
|
+
archive.append(oldest)
|
|
68
|
+
state['error_archive'] = archive
|
|
69
|
+
|
|
70
|
+
state['error_notebook'] = errors
|
|
71
|
+
return state
|
|
72
|
+
|
|
73
|
+
def get_errors_page(self, state: Dict[str, Any],
|
|
74
|
+
page: int = 1,
|
|
75
|
+
per_page: int = 5,
|
|
76
|
+
month: str = None,
|
|
77
|
+
random: int = None) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Get paginated errors from the error notebook.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
state: Current state
|
|
83
|
+
page: Page number (1-indexed)
|
|
84
|
+
per_page: Items per page (default 5)
|
|
85
|
+
month: Filter by month (YYYY-MM format)
|
|
86
|
+
random: Return N random errors instead of paginated
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dictionary with pagination info and error items:
|
|
90
|
+
{
|
|
91
|
+
'total': total_count,
|
|
92
|
+
'page': current_page,
|
|
93
|
+
'per_page': items_per_page,
|
|
94
|
+
'total_pages': total_pages,
|
|
95
|
+
'has_more': bool,
|
|
96
|
+
'errors': [error_items]
|
|
97
|
+
}
|
|
98
|
+
"""
|
|
99
|
+
import random as random_module
|
|
100
|
+
|
|
101
|
+
errors = state.get('error_notebook', [])
|
|
102
|
+
|
|
103
|
+
# Sort by date descending (newest first)
|
|
104
|
+
errors = sorted(errors, key=lambda x: x.get('date', ''), reverse=True)
|
|
105
|
+
|
|
106
|
+
# Filter by month if specified
|
|
107
|
+
if month:
|
|
108
|
+
errors = [e for e in errors if e.get('date', '').startswith(month)]
|
|
109
|
+
|
|
110
|
+
total = len(errors)
|
|
111
|
+
|
|
112
|
+
# Random mode
|
|
113
|
+
if random and random > 0:
|
|
114
|
+
random_count = min(random, total)
|
|
115
|
+
selected = random_module.sample(errors, random_count) if total > 0 else []
|
|
116
|
+
return {
|
|
117
|
+
'total': total,
|
|
118
|
+
'page': 1,
|
|
119
|
+
'per_page': random_count,
|
|
120
|
+
'total_pages': 1,
|
|
121
|
+
'has_more': total > random_count,
|
|
122
|
+
'mode': 'random',
|
|
123
|
+
'errors': selected
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Pagination
|
|
127
|
+
total_pages = (total + per_page - 1) // per_page if per_page > 0 else 1
|
|
128
|
+
page = max(1, min(page, total_pages)) if total_pages > 0 else 1
|
|
129
|
+
|
|
130
|
+
start = (page - 1) * per_page
|
|
131
|
+
end = start + per_page
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
'total': total,
|
|
135
|
+
'page': page,
|
|
136
|
+
'per_page': per_page,
|
|
137
|
+
'total_pages': total_pages,
|
|
138
|
+
'has_more': page < total_pages,
|
|
139
|
+
'has_prev': page > 1,
|
|
140
|
+
'mode': 'paginated',
|
|
141
|
+
'errors': errors[start:end]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def get_error_stats(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Get statistics about the error notebook.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
state: Current state
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary with error statistics
|
|
153
|
+
"""
|
|
154
|
+
errors = state.get('error_notebook', [])
|
|
155
|
+
|
|
156
|
+
if not errors:
|
|
157
|
+
return {
|
|
158
|
+
'total': 0,
|
|
159
|
+
'reviewed': 0,
|
|
160
|
+
'unreviewed': 0,
|
|
161
|
+
'by_month': {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
reviewed = sum(1 for e in errors if e.get('reviewed', False))
|
|
165
|
+
unreviewed = len(errors) - reviewed
|
|
166
|
+
|
|
167
|
+
# Group by month
|
|
168
|
+
by_month = Counter()
|
|
169
|
+
for e in errors:
|
|
170
|
+
date_str = e.get('date', '')
|
|
171
|
+
if date_str:
|
|
172
|
+
month = date_str[:7] # YYYY-MM
|
|
173
|
+
by_month[month] += 1
|
|
174
|
+
|
|
175
|
+
# Sort by month descending
|
|
176
|
+
by_month_sorted = dict(sorted(by_month.items(), reverse=True))
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
'total': len(errors),
|
|
180
|
+
'reviewed': reviewed,
|
|
181
|
+
'unreviewed': unreviewed,
|
|
182
|
+
'by_month': by_month_sorted
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def review_error(self, state: Dict[str, Any], error_index: int,
|
|
186
|
+
correct: bool) -> Dict[str, Any]:
|
|
187
|
+
"""
|
|
188
|
+
Mark an error as reviewed (correct answer) or increment wrong count.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
state: Current state
|
|
192
|
+
error_index: Index of error in error_notebook
|
|
193
|
+
correct: Whether the user answered correctly
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Updated state
|
|
197
|
+
"""
|
|
198
|
+
errors = state.get('error_notebook', [])
|
|
199
|
+
if 0 <= error_index < len(errors):
|
|
200
|
+
if correct:
|
|
201
|
+
# Mark as reviewed - contributes to Error Slayer badge
|
|
202
|
+
errors[error_index]['reviewed'] = True
|
|
203
|
+
else:
|
|
204
|
+
# Increment wrong count
|
|
205
|
+
errors[error_index]['wrong_count'] = errors[error_index].get('wrong_count', 1) + 1
|
|
206
|
+
state['error_notebook'] = errors
|
|
207
|
+
return state
|
|
208
|
+
|
|
209
|
+
def increment_wrong_count(self, state: Dict[str, Any],
|
|
210
|
+
error_index: int) -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Increment wrong count for an error (convenience method).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
state: Current state
|
|
216
|
+
error_index: Index of error in error_notebook
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Updated state
|
|
220
|
+
"""
|
|
221
|
+
return self.review_error(state, error_index, correct=False)
|
|
222
|
+
|
|
223
|
+
def get_review_errors(self, state: Dict[str, Any],
|
|
224
|
+
count: int = 5) -> List[Dict[str, Any]]:
|
|
225
|
+
"""
|
|
226
|
+
Get unreviewed errors for a review session.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
state: Current state
|
|
230
|
+
count: Maximum number of errors to return
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of unreviewed errors (most recent first)
|
|
234
|
+
"""
|
|
235
|
+
errors = state.get('error_notebook', [])
|
|
236
|
+
# Filter unreviewed errors
|
|
237
|
+
unreviewed = [e for e in errors if not e.get('reviewed', False)]
|
|
238
|
+
# Sort by date descending (newest first)
|
|
239
|
+
unreviewed = sorted(unreviewed, key=lambda x: x.get('date', ''), reverse=True)
|
|
240
|
+
# Return up to count items
|
|
241
|
+
return unreviewed[:count]
|
|
242
|
+
|
|
243
|
+
def archive_stale_errors(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Archive errors with wrong_count >= threshold and over threshold days old.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
state: Current state
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Updated state with stale errors moved to error_archive
|
|
252
|
+
"""
|
|
253
|
+
errors = state.get('error_notebook', [])
|
|
254
|
+
archive = state.get('error_archive', [])
|
|
255
|
+
today = date.today()
|
|
256
|
+
|
|
257
|
+
remaining = []
|
|
258
|
+
archived_count = 0
|
|
259
|
+
|
|
260
|
+
for error in errors:
|
|
261
|
+
# Skip already reviewed
|
|
262
|
+
if error.get('reviewed', False):
|
|
263
|
+
remaining.append(error)
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
wrong_count = error.get('wrong_count', 1)
|
|
267
|
+
error_date_str = error.get('date', '')
|
|
268
|
+
|
|
269
|
+
# Calculate days since error was created
|
|
270
|
+
try:
|
|
271
|
+
error_date = datetime.strptime(error_date_str, '%Y-%m-%d').date()
|
|
272
|
+
days_old = (today - error_date).days
|
|
273
|
+
except (ValueError, TypeError):
|
|
274
|
+
days_old = 0
|
|
275
|
+
|
|
276
|
+
# Archive if wrong_count and days_old meet thresholds
|
|
277
|
+
if wrong_count >= ERROR_ARCHIVE_WRONG_THRESHOLD and days_old >= ERROR_ARCHIVE_DAYS_THRESHOLD:
|
|
278
|
+
error['archived_at'] = today.isoformat()
|
|
279
|
+
archive.append(error)
|
|
280
|
+
archived_count += 1
|
|
281
|
+
else:
|
|
282
|
+
remaining.append(error)
|
|
283
|
+
|
|
284
|
+
state['error_notebook'] = remaining
|
|
285
|
+
state['error_archive'] = archive
|
|
286
|
+
|
|
287
|
+
# Log archival event via state_manager
|
|
288
|
+
if archived_count > 0 and self.state_manager:
|
|
289
|
+
self.state_manager.append_event("errors_archived", {"count": archived_count})
|
|
290
|
+
|
|
291
|
+
return state
|
|
292
|
+
|
|
293
|
+
def clear_reviewed_errors(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Remove reviewed errors from the notebook (keep them in archive for stats).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
state: Current state
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Updated state
|
|
302
|
+
"""
|
|
303
|
+
errors = state.get('error_notebook', [])
|
|
304
|
+
# Keep only unreviewed errors
|
|
305
|
+
state['error_notebook'] = [
|
|
306
|
+
e for e in errors if not e.get('reviewed', False)
|
|
307
|
+
]
|
|
308
|
+
return state
|