@rookiestar/eng-lang-tutor 1.0.2 → 1.0.3

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.

Potentially problematic release.


This version of @rookiestar/eng-lang-tutor might be problematic. Click here for more details.

Files changed (35) hide show
  1. package/CLAUDE.md +5 -4
  2. package/README.md +16 -1
  3. package/README_EN.md +16 -1
  4. package/SKILL.md +113 -92
  5. package/docs/OPENCLAW_DEPLOYMENT.md +7 -4
  6. package/package.json +1 -1
  7. package/scripts/__pycache__/cli.cpython-313.pyc +0 -0
  8. package/scripts/__pycache__/command_parser.cpython-313.pyc +0 -0
  9. package/scripts/__pycache__/constants.cpython-313.pyc +0 -0
  10. package/scripts/__pycache__/cron_push.cpython-313.pyc +0 -0
  11. package/scripts/__pycache__/dedup.cpython-313.pyc +0 -0
  12. package/scripts/__pycache__/gamification.cpython-313.pyc +0 -0
  13. package/scripts/__pycache__/scorer.cpython-313.pyc +0 -0
  14. package/scripts/__pycache__/state_manager.cpython-313.pyc +0 -0
  15. package/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  16. package/scripts/cli.py +203 -0
  17. package/scripts/constants.py +56 -0
  18. package/scripts/gamification.py +48 -34
  19. package/scripts/scorer.py +14 -32
  20. package/scripts/state_manager.py +41 -206
  21. package/scripts/tts/__pycache__/__init__.cpython-313.pyc +0 -0
  22. package/scripts/tts/__pycache__/base.cpython-313.pyc +0 -0
  23. package/scripts/tts/__pycache__/manager.cpython-313.pyc +0 -0
  24. package/scripts/tts/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  25. package/scripts/tts/providers/__pycache__/xunfei.cpython-313.pyc +0 -0
  26. package/scripts/utils.py +78 -0
  27. package/templates/prompt_templates.md +47 -1235
  28. package/templates/prompts/display_guide.md +106 -0
  29. package/templates/prompts/initialization.md +213 -0
  30. package/templates/prompts/keypoint_generation.md +268 -0
  31. package/templates/prompts/output_rules.md +106 -0
  32. package/templates/prompts/quiz_generation.md +187 -0
  33. package/templates/prompts/responses.md +291 -0
  34. package/templates/prompts/shared_enums.md +97 -0
  35. package/templates/state_schema.json +1 -6
@@ -16,6 +16,8 @@ from datetime import datetime, date
16
16
  from typing import Dict, Any, Optional, List
17
17
  import shutil
18
18
 
19
+ from utils import deep_merge
20
+
19
21
 
20
22
  def get_default_state_dir() -> Path:
21
23
  """
@@ -116,6 +118,16 @@ class StateManager:
116
118
  except Exception as e:
117
119
  print(f"Warning: Migration failed: {e}")
118
120
  print("Will use new empty state directory.")
121
+ # Log migration failure to event log (after ensuring directories exist)
122
+ try:
123
+ self._ensure_directories()
124
+ self.append_event('migration_failed', {
125
+ "error": str(e),
126
+ "source_dir": str(old_data_dir),
127
+ "target_dir": str(self.data_dir)
128
+ })
129
+ except Exception:
130
+ pass # Silently ignore logging failures during migration
119
131
 
120
132
  def _ensure_directories(self) -> None:
121
133
  """Create necessary directories if they don't exist."""
@@ -163,7 +175,6 @@ class StateManager:
163
175
  "dedup_days": 14
164
176
  },
165
177
  "progress": {
166
- "total_points": 0,
167
178
  "total_quizzes": 0,
168
179
  "correct_rate": 0.0,
169
180
  "last_study_date": None,
@@ -195,26 +206,13 @@ class StateManager:
195
206
  return self._default_state()
196
207
 
197
208
  def _merge_with_defaults(self, state: Dict[str, Any]) -> Dict[str, Any]:
198
- """Merge loaded state with defaults to ensure all fields exist."""
199
- defaults = self._default_state()
200
- merged = defaults.copy()
201
-
202
- # Handle top-level boolean/integer fields
203
- for key in ['initialized', 'onboarding_step', 'version']:
204
- if key in state:
205
- merged[key] = state[key]
206
-
207
- # Handle nested objects
208
- for key in ['user', 'preferences', 'progress', 'completion_status', 'schedule']:
209
- if key in state and isinstance(state[key], dict):
210
- merged[key] = {**defaults.get(key, {}), **state[key]}
211
-
212
- # Handle arrays
213
- for key in ['recent_topics', 'error_notebook', 'error_archive']:
214
- if key in state:
215
- merged[key] = state[key]
209
+ """
210
+ Merge loaded state with defaults to ensure all fields exist.
216
211
 
217
- return merged
212
+ Uses deep merge to handle nested structures properly.
213
+ """
214
+ defaults = self._default_state()
215
+ return deep_merge(defaults, state)
218
216
 
219
217
  def save_state(self, state: Dict[str, Any]) -> None:
220
218
  """
@@ -375,6 +373,9 @@ class StateManager:
375
373
  recent_content = self.get_recent_daily_content(days)
376
374
  return [c.get('topic_fingerprint', '') for c in recent_content if c.get('topic_fingerprint')]
377
375
 
376
+ # Maximum size for error notebook before auto-archiving
377
+ MAX_ERROR_NOTEBOOK_SIZE = 100
378
+
378
379
  def add_to_error_notebook(self, state: Dict[str, Any],
379
380
  error: Dict[str, Any]) -> Dict[str, Any]:
380
381
  """
@@ -391,7 +392,23 @@ class StateManager:
391
392
  error['reviewed'] = False
392
393
  error['wrong_count'] = error.get('wrong_count', 1)
393
394
  # keypoint_date and question_type are optional
394
- state['error_notebook'].append(error)
395
+
396
+ errors = state.get('error_notebook', [])
397
+ errors.append(error)
398
+
399
+ # Auto-archive oldest if over size limit
400
+ if len(errors) > self.MAX_ERROR_NOTEBOOK_SIZE:
401
+ # Sort by date to find oldest
402
+ errors.sort(key=lambda x: x.get('date', ''))
403
+ # Archive the oldest
404
+ oldest = errors.pop(0)
405
+ archive = state.get('error_archive', [])
406
+ oldest['archived_at'] = date.today().isoformat()
407
+ oldest['archived_reason'] = 'notebook_full'
408
+ archive.append(oldest)
409
+ state['error_archive'] = archive
410
+
411
+ state['error_notebook'] = errors
395
412
  return state
396
413
 
397
414
  def get_errors_page(self, state: Dict[str, Any],
@@ -837,189 +854,7 @@ class StateManager:
837
854
  return False
838
855
 
839
856
 
840
- # CLI interface
857
+ # CLI interface - delegated to cli.py for better code organization
841
858
  if __name__ == "__main__":
842
- import argparse
843
-
844
- parser = argparse.ArgumentParser(description="State Manager for eng-lang-tutor")
845
- parser.add_argument('--data-dir', default=None,
846
- help='Data directory path (default: ~/.openclaw/state/eng-lang-tutor or OPENCLAW_STATE_DIR env)')
847
- parser.add_argument('command', nargs='?',
848
- choices=['show', 'backup', 'save_daily', 'record_view',
849
- 'stats', 'config', 'errors', 'schedule'],
850
- help='Command to execute')
851
- parser.add_argument('--content-type', help='Content type for save_daily (keypoint, quiz)')
852
- parser.add_argument('--content', help='JSON content for save_daily')
853
- parser.add_argument('--date', help='Date for content (YYYY-MM-DD format)')
854
- # Errors command options
855
- parser.add_argument('--page', type=int, default=1, help='Page number for errors list')
856
- parser.add_argument('--per-page', type=int, default=5, help='Items per page for errors')
857
- parser.add_argument('--month', help='Filter errors by month (YYYY-MM)')
858
- parser.add_argument('--random', type=int, help='Get N random errors')
859
- parser.add_argument('--stats', action='store_true', help='Get error statistics')
860
- parser.add_argument('--review', type=int, help='Get N errors for review session')
861
- # Config command options
862
- parser.add_argument('--cefr', help='Set CEFR level (A1-C2)')
863
- parser.add_argument('--style', help='Set tutor style')
864
- parser.add_argument('--oral-ratio', type=int, help='Set oral/written ratio (0-100)')
865
- # Schedule command options
866
- parser.add_argument('--keypoint-time', help='Set keypoint push time (HH:MM)')
867
- parser.add_argument('--quiz-time', help='Set quiz push time (HH:MM)')
868
-
869
- args = parser.parse_args()
870
-
871
- sm = StateManager(args.data_dir)
872
-
873
- if args.command == 'show' or not args.command:
874
- state = sm.load_state()
875
- print(json.dumps(state, indent=2, ensure_ascii=False))
876
-
877
- elif args.command == 'backup':
878
- backup_path = sm.backup_state()
879
- print(f"Backup created: {backup_path}")
880
-
881
- elif args.command == 'save_daily':
882
- if not args.content_type or not args.content:
883
- print("Error: --content-type and --content are required for save_daily")
884
- exit(1)
885
-
886
- try:
887
- content = json.loads(args.content)
888
- except json.JSONDecodeError as e:
889
- print(f"Error: Invalid JSON content: {e}")
890
- exit(1)
891
-
892
- target_date = None
893
- if args.date:
894
- try:
895
- target_date = datetime.strptime(args.date, '%Y-%m-%d').date()
896
- except ValueError:
897
- print("Error: Invalid date format. Use YYYY-MM-DD")
898
- exit(1)
899
-
900
- path = sm.save_daily_content(args.content_type, content, target_date)
901
- print(f"Saved to: {path}")
902
-
903
- elif args.command == 'record_view':
904
- target_date = None
905
- if args.date:
906
- try:
907
- target_date = datetime.strptime(args.date, '%Y-%m-%d').date()
908
- except ValueError:
909
- print("Error: Invalid date format. Use YYYY-MM-DD")
910
- exit(1)
911
-
912
- state = sm.load_state()
913
- sm.record_keypoint_view(state, target_date)
914
- sm.save_state(state)
915
- print("View recorded successfully")
916
-
917
- elif args.command == 'stats':
918
- """Display learning progress summary."""
919
- from gamification import GamificationManager
920
- state = sm.load_state()
921
- gm = GamificationManager()
922
- summary = gm.get_progress_summary(state)
923
- print(json.dumps(summary, indent=2, ensure_ascii=False))
924
-
925
- elif args.command == 'config':
926
- """Display or update user configuration."""
927
- state = sm.load_state()
928
-
929
- # If no update options, just show current config
930
- if not any([args.cefr, args.style, args.oral_ratio is not None]):
931
- config = {
932
- "cefr_level": state.get("preferences", {}).get("cefr_level", "B1"),
933
- "tutor_style": state.get("preferences", {}).get("tutor_style", "humorous"),
934
- "oral_ratio": state.get("preferences", {}).get("oral_ratio", 70),
935
- "topic_weights": state.get("preferences", {}).get("topic_weights", {}),
936
- "schedule": state.get("schedule", {})
937
- }
938
- print(json.dumps(config, indent=2, ensure_ascii=False))
939
- else:
940
- # Update configuration
941
- if args.cefr:
942
- if args.cefr not in ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']:
943
- print("Error: Invalid CEFR level. Must be A1, A2, B1, B2, C1, or C2")
944
- exit(1)
945
- state = sm.update_preferences(state, cefr_level=args.cefr)
946
- print(f"Updated CEFR level to: {args.cefr}")
947
-
948
- if args.style:
949
- if args.style not in ['humorous', 'rigorous', 'casual', 'professional']:
950
- print("Error: Invalid style. Must be humorous, rigorous, casual, or professional")
951
- exit(1)
952
- state = sm.update_preferences(state, tutor_style=args.style)
953
- print(f"Updated tutor style to: {args.style}")
954
-
955
- if args.oral_ratio is not None:
956
- if not 0 <= args.oral_ratio <= 100:
957
- print("Error: Oral ratio must be between 0 and 100")
958
- exit(1)
959
- state = sm.update_preferences(state, oral_ratio=args.oral_ratio)
960
- print(f"Updated oral ratio to: {args.oral_ratio}%")
961
-
962
- sm.save_state(state)
963
- print("Configuration updated successfully")
964
-
965
- elif args.command == 'errors':
966
- """Error notebook operations."""
967
- state = sm.load_state()
968
-
969
- if args.stats:
970
- # Get error statistics
971
- stats = sm.get_error_stats(state)
972
- print(json.dumps(stats, indent=2, ensure_ascii=False))
973
-
974
- elif args.review is not None:
975
- # Get errors for review session
976
- errors = sm.get_review_errors(state, count=args.review)
977
- result = {
978
- "count": len(errors),
979
- "errors": errors
980
- }
981
- print(json.dumps(result, indent=2, ensure_ascii=False))
982
-
983
- else:
984
- # Get paginated errors list
985
- result = sm.get_errors_page(
986
- state,
987
- page=args.page,
988
- per_page=args.per_page,
989
- month=args.month,
990
- random=args.random
991
- )
992
- print(json.dumps(result, indent=2, ensure_ascii=False))
993
-
994
- elif args.command == 'schedule':
995
- """Display or update schedule configuration."""
996
- state = sm.load_state()
997
-
998
- # If no update options, just show current schedule
999
- if not any([args.keypoint_time, args.quiz_time]):
1000
- schedule = state.get("schedule", {})
1001
- print(json.dumps(schedule, indent=2, ensure_ascii=False))
1002
- else:
1003
- # Validate quiz_time must be later than keypoint_time
1004
- current_keypoint = state.get("schedule", {}).get("keypoint_time", "06:45")
1005
- current_quiz = state.get("schedule", {}).get("quiz_time", "22:45")
1006
-
1007
- new_keypoint = args.keypoint_time or current_keypoint
1008
- new_quiz = args.quiz_time or current_quiz
1009
-
1010
- # Time validation
1011
- def parse_time(t):
1012
- h, m = map(int, t.split(':'))
1013
- return h * 60 + m
1014
-
1015
- if parse_time(new_quiz) <= parse_time(new_keypoint):
1016
- print("Error: Quiz time must be later than keypoint time")
1017
- exit(1)
1018
-
1019
- state = sm.update_schedule(
1020
- state,
1021
- keypoint_time=new_keypoint,
1022
- quiz_time=new_quiz
1023
- )
1024
- sm.save_state(state)
1025
- print(f"Schedule updated: keypoint at {new_keypoint}, quiz at {new_quiz}")
859
+ from cli import main
860
+ main()
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utility functions for eng-lang-tutor.
4
+
5
+ Common utilities used across multiple modules.
6
+ """
7
+
8
+ from typing import Dict, Any
9
+
10
+
11
+ def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
12
+ """
13
+ Safe division that returns default if denominator is zero.
14
+
15
+ Args:
16
+ numerator: The number to divide
17
+ denominator: The number to divide by
18
+ default: Value to return if denominator is zero
19
+
20
+ Returns:
21
+ Result of division, or default if denominator is zero
22
+
23
+ Examples:
24
+ >>> safe_divide(10, 2)
25
+ 5.0
26
+ >>> safe_divide(10, 0)
27
+ 0.0
28
+ >>> safe_divide(10, 0, default=100)
29
+ 100.0
30
+ """
31
+ return numerator / denominator if denominator != 0 else default
32
+
33
+
34
+ def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
35
+ """
36
+ Recursively merge override dictionary into base dictionary.
37
+
38
+ Creates a new dictionary with values from base, updated with values from override.
39
+ Nested dictionaries are merged recursively; other values are overwritten.
40
+
41
+ Args:
42
+ base: Base dictionary (not modified)
43
+ override: Dictionary with values to override/add
44
+
45
+ Returns:
46
+ New merged dictionary
47
+
48
+ Examples:
49
+ >>> base = {'a': 1, 'b': {'c': 2, 'd': 3}}
50
+ >>> override = {'b': {'c': 10}}
51
+ >>> deep_merge(base, override)
52
+ {'a': 1, 'b': {'c': 10, 'd': 3}}
53
+ """
54
+ import copy
55
+ result = copy.deepcopy(base)
56
+
57
+ for key, value in override.items():
58
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
59
+ result[key] = deep_merge(result[key], value)
60
+ else:
61
+ result[key] = copy.deepcopy(value)
62
+
63
+ return result
64
+
65
+
66
+ def clamp(value: float, min_val: float, max_val: float) -> float:
67
+ """
68
+ Clamp a value to a range.
69
+
70
+ Args:
71
+ value: Value to clamp
72
+ min_val: Minimum allowed value
73
+ max_val: Maximum allowed value
74
+
75
+ Returns:
76
+ Value clamped to [min_val, max_val]
77
+ """
78
+ return max(min_val, min(max_val, value))