@rookiestar/eng-lang-tutor 1.0.0
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.
- package/CLAUDE.md +259 -0
- package/README.md +224 -0
- package/README_EN.md +224 -0
- package/SKILL.md +495 -0
- package/bin/eng-lang-tutor.js +177 -0
- package/docs/OPENCLAW_DEPLOYMENT.md +228 -0
- package/examples/sample_keypoint.json +130 -0
- package/examples/sample_quiz.json +92 -0
- package/npm-scripts/install.js +132 -0
- package/package.json +46 -0
- package/references/resources.md +292 -0
- package/requirements.txt +9 -0
- package/scripts/command_parser.py +336 -0
- package/scripts/cron_push.py +226 -0
- package/scripts/dedup.py +325 -0
- package/scripts/gamification.py +406 -0
- package/scripts/scorer.py +323 -0
- package/scripts/state_manager.py +1025 -0
- package/scripts/tts/__init__.py +30 -0
- package/scripts/tts/base.py +109 -0
- package/scripts/tts/manager.py +290 -0
- package/scripts/tts/providers/__init__.py +10 -0
- package/scripts/tts/providers/xunfei.py +192 -0
- package/templates/keypoint_schema.json +420 -0
- package/templates/prompt_templates.md +1261 -0
- package/templates/quiz_schema.json +201 -0
- package/templates/state_schema.json +241 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# English Learning Resources
|
|
2
|
+
|
|
3
|
+
> This file contains themed resources for generating authentic American English content.
|
|
4
|
+
> LLM should reference these resources when generating knowledge points and quizzes.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Native American English Expressions
|
|
9
|
+
|
|
10
|
+
### 1.1 Classic Learning Materials
|
|
11
|
+
|
|
12
|
+
| Resource | Description | Key Features |
|
|
13
|
+
|----------|-------------|--------------|
|
|
14
|
+
| **Speak English Like an American** | Classic textbook with 350+ idioms | Dialogue-based learning, everyday situations |
|
|
15
|
+
| **NTC's Dictionary of American Slang** | Comprehensive slang dictionary | Colloquial expressions, usage examples |
|
|
16
|
+
| **American Colloquial Expressions** | Collection of spoken phrases | Almost all idioms are colloquial language |
|
|
17
|
+
|
|
18
|
+
### 1.2 Common Chinglish Corrections
|
|
19
|
+
|
|
20
|
+
| Chinglish (Wrong) | American (Correct) | Explanation |
|
|
21
|
+
|-------------------|-------------------|-------------|
|
|
22
|
+
| "It was just so-so." | "It was just ok/alright." | Americans rarely use "so-so" |
|
|
23
|
+
| "Let's go out to play." | "Let's go out!" | "Play" is for children; adults "go out" |
|
|
24
|
+
| "Bad for your health." | "It's not good for you." | More natural phrasing |
|
|
25
|
+
| "I very like it." | "I really like it." | "Very" doesn't modify "like" |
|
|
26
|
+
| "How to say?" | "How do I put this?" / "What's the word?" | Natural hesitation phrases |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. TV Shows & Movies (Topic: movies)
|
|
31
|
+
|
|
32
|
+
### 2.1 Recommended Shows for Learning
|
|
33
|
+
|
|
34
|
+
| Show | Genre | Learning Focus | Classic Lines |
|
|
35
|
+
|------|-------|----------------|---------------|
|
|
36
|
+
| **Friends** | Sitcom | Daily conversation, humor, sarcasm | "How you doin'?", "Oh. My. God!" |
|
|
37
|
+
| **The Office** | Workplace Comedy | Office idioms, corporate culture | "That's what she said", water-cooler chat |
|
|
38
|
+
| **Gossip Girl** | Teen Drama | Youth slang, fashion, social dynamics | "No offense." / "None taken.", "Done and done." |
|
|
39
|
+
| **Desperate Housewives** | Drama | Formal/informal switching, life situations | "Take a rain check", "Serves him right" |
|
|
40
|
+
| **House of Cards** | Political Thriller | Business/political vocabulary, formal speech | Complex sentence structures |
|
|
41
|
+
| **Modern Family** | Sitcom | Family dynamics, generational differences | "I'm not a regular mom, I'm a cool mom" |
|
|
42
|
+
| **How I Met Your Mother** | Sitcom | Dating slang, friendship expressions | "Legendary", "Suit up!" |
|
|
43
|
+
|
|
44
|
+
### 2.2 Expression Examples from TV Shows
|
|
45
|
+
|
|
46
|
+
**From Friends:**
|
|
47
|
+
- "We were on a break!" - Defending oneself
|
|
48
|
+
- "Could I BE any more...?" - Emphasis (Chandler's style)
|
|
49
|
+
- "Pivot!" - When moving something difficult
|
|
50
|
+
|
|
51
|
+
**From The Office:**
|
|
52
|
+
- "Touch base" - Briefly connect with someone
|
|
53
|
+
- "Circle back" - Return to a topic later
|
|
54
|
+
- "Low-hanging fruit" - Easy wins
|
|
55
|
+
|
|
56
|
+
**From Gossip Girl:**
|
|
57
|
+
- "Any interest in fresh air?" - Casual invitation
|
|
58
|
+
- "You set me up!" - Accusing someone of a trap
|
|
59
|
+
- "It was a last minute thing." - Explaining spontaneity
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. News & Media (Topic: news)
|
|
64
|
+
|
|
65
|
+
### 3.1 News Sources for Learning
|
|
66
|
+
|
|
67
|
+
| Source | Accent | Features | Topics |
|
|
68
|
+
|--------|--------|----------|--------|
|
|
69
|
+
| **CNN 10** | American | 10-min daily news for students | Current events, explained clearly |
|
|
70
|
+
| **VOA Learning English** | American | Slow pace, simplified vocabulary | World news, American life |
|
|
71
|
+
| **BBC Learning English** | British/American | Grammar focus, pronunciation | General English |
|
|
72
|
+
| **Engoo** | Various | Graded by difficulty, daily updates | News, culture, lifestyle |
|
|
73
|
+
| **NPR** | American | Natural pace, diverse topics | Culture, science, stories |
|
|
74
|
+
|
|
75
|
+
### 3.2 News English Vocabulary
|
|
76
|
+
|
|
77
|
+
**Politics & Government:**
|
|
78
|
+
- bill, veto, legislation, incumbent, poll, constituency
|
|
79
|
+
- "The bill passed with a narrow margin."
|
|
80
|
+
- "The incumbent is seeking re-election."
|
|
81
|
+
|
|
82
|
+
**Business & Economy:**
|
|
83
|
+
- earnings, quarterly, forecast, market trends, inflation
|
|
84
|
+
- "Stocks rallied after the announcement."
|
|
85
|
+
- "The company exceeded earnings expectations."
|
|
86
|
+
|
|
87
|
+
**Technology:**
|
|
88
|
+
- breakthrough, innovation, cybersecurity, AI, startup
|
|
89
|
+
- "The startup secured Series B funding."
|
|
90
|
+
- "The breakthrough could revolutionize..."
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Gaming (Topic: gaming)
|
|
95
|
+
|
|
96
|
+
### 4.1 Gaming Terminology
|
|
97
|
+
|
|
98
|
+
| Category | Terms & Expressions |
|
|
99
|
+
|----------|-------------------|
|
|
100
|
+
| **Core Terms** | NPC, spawn, respawn, loot, grind, level up, XP, HP, MP |
|
|
101
|
+
| **Gameplay** | buff, nerf, patch, DLC, speedrun, playthrough |
|
|
102
|
+
| **Multiplayer** | party, squad, team up, PvP, PvE, co-op, matchmaking, lobby |
|
|
103
|
+
| **Competition** | GG (good game), noob, pro, clutch, carry, feed, throw, smurf |
|
|
104
|
+
| **Actions** | camp, kite, tank, heal, revive, reload, flank, rush |
|
|
105
|
+
|
|
106
|
+
### 4.2 Gaming Expressions in Daily Use
|
|
107
|
+
|
|
108
|
+
| Expression | Meaning | Example |
|
|
109
|
+
|------------|---------|---------|
|
|
110
|
+
| "GG" | Good game, well done | "That presentation was GG." |
|
|
111
|
+
| "Nerf" | Weaken or reduce effectiveness | "They nerfed the office coffee machine." |
|
|
112
|
+
| "Grind" | Repetitive hard work | "I'm grinding through these reports." |
|
|
113
|
+
| "Carry" | Do most of the work | "She carried the whole project." |
|
|
114
|
+
| "Clutch" | Success under pressure | "That was a clutch presentation." |
|
|
115
|
+
| "Buff up" | Improve or strengthen | "Need to buff up my resume." |
|
|
116
|
+
|
|
117
|
+
### 4.3 Gaming Dialogue Examples
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
A: "Wanna squad up tonight?"
|
|
121
|
+
B: "Sure, I'll be on around 8."
|
|
122
|
+
A: "Cool, let's grind some ranked matches."
|
|
123
|
+
B: "Hopefully we don't get matched with noobs."
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 5. Sports (Topic: sports)
|
|
129
|
+
|
|
130
|
+
### 5.1 Basketball Vocabulary
|
|
131
|
+
|
|
132
|
+
| Category | Terms |
|
|
133
|
+
|----------|-------|
|
|
134
|
+
| **Scoring** | dunk, layup, three-pointer, free throw, buzzer beater |
|
|
135
|
+
| **Defense** | block, steal, rebound, turnover, foul |
|
|
136
|
+
| **Tactics** | pick and roll, fast break, zone defense, press |
|
|
137
|
+
| **Positions** | point guard, shooting guard, forward, center |
|
|
138
|
+
|
|
139
|
+
**Common Expressions:**
|
|
140
|
+
- "He hit a buzzer beater to win the game!"
|
|
141
|
+
- "She's on fire tonight!" (scoring consistently)
|
|
142
|
+
- "They're running away with this one." (winning easily)
|
|
143
|
+
- "It's a game of runs." (momentum shifts)
|
|
144
|
+
|
|
145
|
+
### 5.2 Sports Idioms in Business/Daily Life
|
|
146
|
+
|
|
147
|
+
| Idiom | Meaning | Example |
|
|
148
|
+
|-------|---------|---------|
|
|
149
|
+
| "Step up to the plate" | Take responsibility | "It's time to step up to the plate." |
|
|
150
|
+
| "Ballpark figure" | Rough estimate | "Give me a ballpark figure." |
|
|
151
|
+
| "Drop the ball" | Make a mistake | "Don't drop the ball on this project." |
|
|
152
|
+
| "Hit it out of the park" | Do excellently | "She hit it out of the park with that pitch." |
|
|
153
|
+
| "On the ball" | Alert, competent | "He's really on the ball today." |
|
|
154
|
+
| "Full court press" | Intense effort | "We need a full court press on this deal." |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. Workplace & Business (Topic: workplace)
|
|
159
|
+
|
|
160
|
+
### 6.1 Office Idioms
|
|
161
|
+
|
|
162
|
+
| Expression | Meaning | Context |
|
|
163
|
+
|------------|---------|---------|
|
|
164
|
+
| "Touch base" | Briefly connect | "Let's touch base next week." |
|
|
165
|
+
| "Circle back" | Return to a topic | "Let's circle back on this later." |
|
|
166
|
+
| "Low-hanging fruit" | Easy wins | "Let's tackle the low-hanging fruit first." |
|
|
167
|
+
| "Bandwidth" | Capacity/availability | "I don't have the bandwidth for this." |
|
|
168
|
+
| "Synergy" | Combined effect | "Looking for synergy between teams." |
|
|
169
|
+
| "Pain point" | Problem area | "What are the customer pain points?" |
|
|
170
|
+
| "Value-add" | Benefit provided | "What's the value-add here?" |
|
|
171
|
+
| "Action items" | Tasks to complete | "Let's review the action items." |
|
|
172
|
+
|
|
173
|
+
### 6.2 Meeting Phrases
|
|
174
|
+
|
|
175
|
+
**Starting:**
|
|
176
|
+
- "Let's get the ball rolling."
|
|
177
|
+
- "Shall we dive in?"
|
|
178
|
+
- "Thanks for hopping on this call."
|
|
179
|
+
|
|
180
|
+
**Contributing:**
|
|
181
|
+
- "I'd like to add something here."
|
|
182
|
+
- "Building on what [name] said..."
|
|
183
|
+
- "Here's my take on this."
|
|
184
|
+
|
|
185
|
+
**Disagreeing Politely:**
|
|
186
|
+
- "I see where you're coming from, but..."
|
|
187
|
+
- "That's an interesting perspective. Have we considered...?"
|
|
188
|
+
- "I'm not sure I entirely agree."
|
|
189
|
+
|
|
190
|
+
**Ending:**
|
|
191
|
+
- "Let's wrap this up."
|
|
192
|
+
- "To summarize, we've agreed on..."
|
|
193
|
+
- "I think we're good to go."
|
|
194
|
+
|
|
195
|
+
### 6.3 Workplace Dialogue Examples
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
A: "Hey, do you have a minute to touch base on the project?"
|
|
199
|
+
B: "Sure, what's up?"
|
|
200
|
+
A: "I wanted to circle back on the timeline. We might need more bandwidth."
|
|
201
|
+
B: "Got it. Let's identify the pain points and see what low-hanging fruit we can tackle first."
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 7. Daily Life (Topic: daily_life)
|
|
207
|
+
|
|
208
|
+
### 7.1 Shopping Expressions
|
|
209
|
+
|
|
210
|
+
| Situation | Expressions |
|
|
211
|
+
|-----------|-------------|
|
|
212
|
+
| **Asking for help** | "Can I help you?" / "Just looking, thanks." / "Where can I find...?" |
|
|
213
|
+
| **Asking about products** | "Do you have this in a different size/color?" / "Is this on sale?" |
|
|
214
|
+
| **Trying on** | "Can I try this on?" / "Where's the fitting room?" |
|
|
215
|
+
| **Price negotiation** | "Is this the best price?" / "Can you give me a discount?" |
|
|
216
|
+
| **At checkout** | "I'll take it." / "Do you take credit cards?" / "Can I get a receipt?" |
|
|
217
|
+
|
|
218
|
+
### 7.2 Restaurant Expressions
|
|
219
|
+
|
|
220
|
+
| Situation | Expressions |
|
|
221
|
+
|-----------|-------------|
|
|
222
|
+
| **Ordering** | "I'd like the..." / "Could I get...?" / "What do you recommend?" |
|
|
223
|
+
| **Special requests** | "Could I get this without...?" / "Is this vegetarian?" |
|
|
224
|
+
| **During meal** | "Could I get some more water?" / "This is delicious!" |
|
|
225
|
+
| **Paying** | "Could I get the check, please?" / "Separate checks, please." |
|
|
226
|
+
| **To-go** | "Can I get this to go?" / "I'll have the rest to go." |
|
|
227
|
+
|
|
228
|
+
### 7.3 Social Expressions
|
|
229
|
+
|
|
230
|
+
**Greetings:**
|
|
231
|
+
- "What's up?" / "How's it going?" / "How have you been?"
|
|
232
|
+
- "Long time no see!" / "Good to see you!"
|
|
233
|
+
|
|
234
|
+
**Casual Responses:**
|
|
235
|
+
- "Not much, you?" / "Can't complain." / "Same old, same old."
|
|
236
|
+
- "Take it easy!" / "Catch you later!" / "See ya!"
|
|
237
|
+
|
|
238
|
+
**Making Plans:**
|
|
239
|
+
- "We should hang out sometime." / "Let's grab coffee."
|
|
240
|
+
- "Are you free this weekend?" / "What are you up to tonight?"
|
|
241
|
+
|
|
242
|
+
**Polite Refusals:**
|
|
243
|
+
- "I'd love to, but I can't make it." / "Maybe another time."
|
|
244
|
+
- "I'll have to take a rain check."
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 8. Casual & Slang (All Topics)
|
|
249
|
+
|
|
250
|
+
### 8.1 Common American Slang
|
|
251
|
+
|
|
252
|
+
| Slang | Meaning | Example |
|
|
253
|
+
|-------|---------|---------|
|
|
254
|
+
| "Gonna" | Going to | "I'm gonna grab some food." |
|
|
255
|
+
| "Gotta" | Have to | "I gotta run." |
|
|
256
|
+
| "Wanna" | Want to | "Do you wanna come?" |
|
|
257
|
+
| "Kinda" | Kind of | "It's kinda cold today." |
|
|
258
|
+
| "Lemme" | Let me | "Lemme see that." |
|
|
259
|
+
| "Dunno" | Don't know | "I dunno, maybe?" |
|
|
260
|
+
| "Ya" | You | "See ya later!" |
|
|
261
|
+
| "Y'all" | You all | "Y'all coming?" |
|
|
262
|
+
|
|
263
|
+
### 8.2 Fillers and Hesitation
|
|
264
|
+
|
|
265
|
+
| Filler | Usage |
|
|
266
|
+
|--------|-------|
|
|
267
|
+
| "Like" | "It was, like, really expensive." |
|
|
268
|
+
| "You know" | "It's that place, you know, near the mall." |
|
|
269
|
+
| "I mean" | "I mean, I could try, but..." |
|
|
270
|
+
| "Sort of / Kind of" | "It's sort of complicated." |
|
|
271
|
+
| "Basically" | "Basically, what happened was..." |
|
|
272
|
+
| "Actually" | "Actually, I think you're right." |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 9. Usage Notes for LLM
|
|
277
|
+
|
|
278
|
+
When generating content, reference these resources based on the selected topic:
|
|
279
|
+
|
|
280
|
+
1. **Movies**: Use expressions from TV shows section
|
|
281
|
+
2. **News**: Reference news vocabulary and formal expressions
|
|
282
|
+
3. **Gaming**: Include gaming terminology and slang
|
|
283
|
+
4. **Sports**: Use sports idioms and expressions
|
|
284
|
+
5. **Workplace**: Focus on office idioms and meeting phrases
|
|
285
|
+
6. **Social**: Use casual greetings and social expressions
|
|
286
|
+
7. **Daily Life**: Reference shopping/restaurant dialogues
|
|
287
|
+
|
|
288
|
+
Always include:
|
|
289
|
+
- Scene context matching the topic
|
|
290
|
+
- Chinglish trap with correction
|
|
291
|
+
- Natural American alternatives
|
|
292
|
+
- Pronunciation tips for casual speech (gonna, gotta, etc.)
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Command Parser - Parses user messages and routes to appropriate handlers.
|
|
4
|
+
|
|
5
|
+
Handles:
|
|
6
|
+
- Command recognition via regex patterns
|
|
7
|
+
- Parameter extraction from messages
|
|
8
|
+
- Initialization flow detection
|
|
9
|
+
- Bilingual support (English/Chinese)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Dict, Any, Optional, Tuple
|
|
14
|
+
from datetime import datetime, date, timedelta
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandParser:
|
|
18
|
+
"""Parses user messages to determine intent and extract parameters."""
|
|
19
|
+
|
|
20
|
+
# Command patterns with bilingual support
|
|
21
|
+
COMMAND_PATTERNS = {
|
|
22
|
+
# Initialization commands
|
|
23
|
+
"init_start": r"(?i)^(start|begin|开始|初始化|你好|hello|hi|嗨|hey).*$",
|
|
24
|
+
|
|
25
|
+
# Keypoint commands
|
|
26
|
+
"keypoint_today": r"(?i)(keypoint|知识点|今天|today).*(?!.*(历史|history|昨天|yesterday|前天|上周))",
|
|
27
|
+
"keypoint_history": r"(?i)(keypoint|知识点).*(历史|history|昨天|yesterday|前天|previous|上周)",
|
|
28
|
+
"keypoint_date": r"(?i)(keypoint|知识点).*?(\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}[日号]?)",
|
|
29
|
+
|
|
30
|
+
# Quiz commands
|
|
31
|
+
"quiz_take": r"(?i)(quiz|测验|test|测试|答题|考试)",
|
|
32
|
+
|
|
33
|
+
# Stats commands
|
|
34
|
+
"stats_view": r"(?i)(stats|progress|进度|统计|等级|level|xp|连胜|streak|成就|achievement)",
|
|
35
|
+
|
|
36
|
+
# Config commands
|
|
37
|
+
"config_view": r"(?i)(config|settings?|设置|配置|偏好|preference)(?!.*(change|改|set|设|更新))",
|
|
38
|
+
"config_change_cefr": r"(?i)(cefr|等级|level).*(A1|A2|B1|B2|C1|C2)",
|
|
39
|
+
"config_change_style": r"(?i)(style|风格|导师).*(humorous|rigorous|casual|professional|幽默|严谨|随意|专业)",
|
|
40
|
+
"config_change_topics": r"(?i)(topic|主题|配比|权重|兴趣)",
|
|
41
|
+
"config_change_ratio": r"(?i)(ratio|比例).*(口语|oral|书面|written|speaking|writing)",
|
|
42
|
+
|
|
43
|
+
# Schedule commands
|
|
44
|
+
"schedule_view": r"(?i)(schedule|时间表|推送时间|定时)(?!.*(change|改|set|设))",
|
|
45
|
+
"schedule_change": r"(?i)(schedule|时间表|推送时间).*(change|改|set|设|调整)",
|
|
46
|
+
|
|
47
|
+
# Error notebook commands
|
|
48
|
+
"errors_view": r"(?i)(errors?|错题本|mistakes?|wrong|错误|错题)(?!.*(更多|more|随机|random|清空|clear|删除|remove|\d{4}-\d{2}))",
|
|
49
|
+
"errors_more": r"(?i)(errors?|错题本).*(更多|more|下一页|next)",
|
|
50
|
+
"errors_page": r"(?i)(errors?|错题本).*(第\s*\d+\s*页|page\s*\d+)",
|
|
51
|
+
"errors_month": r"(?i)(errors?|错题本).*(\d{4}-\d{2})(?!.*\d{2})", # YYYY-MM format
|
|
52
|
+
"errors_random": r"(?i)(errors?|错题本).*(随机|random)",
|
|
53
|
+
"errors_clear": r"(?i)(errors?|错题本).*(clear|清空|删除|remove)",
|
|
54
|
+
"errors_stats": r"(?i)(errors?|错题本).*(统计|stats|statistics)",
|
|
55
|
+
"errors_review": r"(?i)(errors?|错题本).*(复习|review|练习|practice)(?!.*(更多|more|随机|random|清空|clear|删除|remove|统计|stats))",
|
|
56
|
+
|
|
57
|
+
# Help command
|
|
58
|
+
"help": r"(?i)(help|帮助|usage|怎么用|how to use|command|命令|指令|功能)",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Onboarding step patterns for collecting user input
|
|
62
|
+
ONBOARDING_PATTERNS = {
|
|
63
|
+
"cefr_level": r"(?i)(A1|A2|B1|B2|C1|C2)",
|
|
64
|
+
"tutor_style": r"(?i)(humorous|rigorous|casual|professional|幽默|严谨|随意|专业)",
|
|
65
|
+
"topics": r"(?i)(movies?|新闻?|news|游戏?|gaming|体育?|sports?|职场?|workplace|社交?|social|生活?|daily)",
|
|
66
|
+
"ratio": r"(\d{1,3})\s*(%|percent|百分比)?",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def __init__(self, state_manager=None):
|
|
70
|
+
"""
|
|
71
|
+
Initialize the command parser.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
state_manager: Optional StateManager instance for context
|
|
75
|
+
"""
|
|
76
|
+
self.state_manager = state_manager
|
|
77
|
+
|
|
78
|
+
def parse(self, message: str, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Parse user message and return command info.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
message: User's message text
|
|
84
|
+
state: Current state dictionary
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dictionary with:
|
|
88
|
+
- command: Command name
|
|
89
|
+
- params: Extracted parameters
|
|
90
|
+
- requires_init: Whether command requires initialization
|
|
91
|
+
- onboarding_input: If in onboarding, the detected input type
|
|
92
|
+
"""
|
|
93
|
+
# Check if user is in onboarding flow
|
|
94
|
+
if not state.get("initialized", False):
|
|
95
|
+
return self._handle_uninitialized(message, state)
|
|
96
|
+
|
|
97
|
+
# Parse for initialized users
|
|
98
|
+
return self._parse_command(message)
|
|
99
|
+
|
|
100
|
+
def _handle_uninitialized(self, message: str, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
+
"""Handle messages from uninitialized users."""
|
|
102
|
+
step = state.get("onboarding_step", 0)
|
|
103
|
+
|
|
104
|
+
if step == 0:
|
|
105
|
+
# Check for start command
|
|
106
|
+
if re.search(self.COMMAND_PATTERNS["init_start"], message):
|
|
107
|
+
return {
|
|
108
|
+
"command": "init_start",
|
|
109
|
+
"params": {},
|
|
110
|
+
"requires_init": False,
|
|
111
|
+
"onboarding_input": None
|
|
112
|
+
}
|
|
113
|
+
else:
|
|
114
|
+
# Any other message triggers welcome
|
|
115
|
+
return {
|
|
116
|
+
"command": "init_welcome",
|
|
117
|
+
"params": {},
|
|
118
|
+
"requires_init": False,
|
|
119
|
+
"onboarding_input": None
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# User is in onboarding, detect what they're providing
|
|
123
|
+
onboarding_input = self._detect_onboarding_input(message, step)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"command": "init_continue",
|
|
127
|
+
"params": {"step": step},
|
|
128
|
+
"requires_init": False,
|
|
129
|
+
"onboarding_input": onboarding_input
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def _detect_onboarding_input(self, message: str, step: int) -> Optional[Dict[str, Any]]:
|
|
133
|
+
"""Detect user input during onboarding based on current step."""
|
|
134
|
+
result = {"type": None, "value": None}
|
|
135
|
+
|
|
136
|
+
if step == 1: # CEFR level
|
|
137
|
+
match = re.search(self.ONBOARDING_PATTERNS["cefr_level"], message)
|
|
138
|
+
if match:
|
|
139
|
+
result = {"type": "cefr_level", "value": match.group(1).upper()}
|
|
140
|
+
|
|
141
|
+
elif step == 2: # Topics
|
|
142
|
+
topics = {}
|
|
143
|
+
topic_keywords = {
|
|
144
|
+
"movies": ["movie", "film", "影视", "电影", "美剧"],
|
|
145
|
+
"news": ["news", "新闻"],
|
|
146
|
+
"gaming": ["game", "gaming", "游戏"],
|
|
147
|
+
"sports": ["sport", "sports", "体育", "运动"],
|
|
148
|
+
"workplace": ["work", "workplace", "office", "职场", "工作"],
|
|
149
|
+
"social": ["social", "社交"],
|
|
150
|
+
"daily_life": ["daily", "life", "生活", "日常"]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for topic, keywords in topic_keywords.items():
|
|
154
|
+
for kw in keywords:
|
|
155
|
+
if kw.lower() in message.lower():
|
|
156
|
+
topics[topic] = 0.2 # Default weight
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if topics:
|
|
160
|
+
# Normalize weights to sum to 1.0
|
|
161
|
+
total = sum(topics.values())
|
|
162
|
+
topics = {k: round(v / total, 2) for k, v in topics.items()}
|
|
163
|
+
result = {"type": "topics", "value": topics}
|
|
164
|
+
|
|
165
|
+
elif step == 3: # Tutor style
|
|
166
|
+
style_map = {
|
|
167
|
+
"humorous": ["humorous", "幽默"],
|
|
168
|
+
"rigorous": ["rigorous", "严谨"],
|
|
169
|
+
"casual": ["casual", "随意", "轻松"],
|
|
170
|
+
"professional": ["professional", "专业"]
|
|
171
|
+
}
|
|
172
|
+
for style, keywords in style_map.items():
|
|
173
|
+
for kw in keywords:
|
|
174
|
+
if kw.lower() in message.lower():
|
|
175
|
+
result = {"type": "tutor_style", "value": style}
|
|
176
|
+
break
|
|
177
|
+
if result["value"]:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
elif step == 4: # Oral/written ratio
|
|
181
|
+
match = re.search(self.ONBOARDING_PATTERNS["ratio"], message)
|
|
182
|
+
if match:
|
|
183
|
+
ratio = int(match.group(1)) / 100.0
|
|
184
|
+
ratio = max(0, min(1, ratio)) # Clamp to 0-1
|
|
185
|
+
result = {"type": "oral_written_ratio", "value": ratio}
|
|
186
|
+
|
|
187
|
+
return result if result["type"] else None
|
|
188
|
+
|
|
189
|
+
def _parse_command(self, message: str) -> Dict[str, Any]:
|
|
190
|
+
"""Parse message for an initialized user."""
|
|
191
|
+
result = {
|
|
192
|
+
"command": "unknown",
|
|
193
|
+
"params": {},
|
|
194
|
+
"requires_init": False,
|
|
195
|
+
"onboarding_input": None
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Check each command pattern
|
|
199
|
+
for cmd_name, pattern in self.COMMAND_PATTERNS.items():
|
|
200
|
+
match = re.search(pattern, message)
|
|
201
|
+
if match:
|
|
202
|
+
result["command"] = cmd_name
|
|
203
|
+
result["params"] = self._extract_params(cmd_name, match, message)
|
|
204
|
+
result["requires_init"] = not cmd_name.startswith("init") and cmd_name != "help"
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
def _extract_params(self, cmd_name: str, match: re.Match, message: str) -> Dict[str, Any]:
|
|
210
|
+
"""Extract parameters from matched command."""
|
|
211
|
+
params = {}
|
|
212
|
+
|
|
213
|
+
# Extract date from keypoint queries
|
|
214
|
+
if "date" in cmd_name or cmd_name in ["keypoint_today", "keypoint_history"]:
|
|
215
|
+
date_match = re.search(r"(\d{4}-\d{2}-\d{2})", message)
|
|
216
|
+
if date_match:
|
|
217
|
+
params["date"] = date_match.group(1)
|
|
218
|
+
elif "history" in cmd_name or "昨天" in message or "yesterday" in message.lower():
|
|
219
|
+
params["date"] = (date.today() - timedelta(days=1)).isoformat()
|
|
220
|
+
elif "前天" in message:
|
|
221
|
+
params["date"] = (date.today() - timedelta(days=2)).isoformat()
|
|
222
|
+
else:
|
|
223
|
+
params["date"] = date.today().isoformat()
|
|
224
|
+
|
|
225
|
+
# Extract CEFR level
|
|
226
|
+
if "cefr" in cmd_name:
|
|
227
|
+
level_match = re.search(r"(A1|A2|B1|B2|C1|C2)", message, re.I)
|
|
228
|
+
if level_match:
|
|
229
|
+
params["cefr_level"] = level_match.group(1).upper()
|
|
230
|
+
|
|
231
|
+
# Extract style
|
|
232
|
+
if "style" in cmd_name:
|
|
233
|
+
style_map = {
|
|
234
|
+
"humorous": "humorous", "幽默": "humorous",
|
|
235
|
+
"rigorous": "rigorous", "严谨": "rigorous",
|
|
236
|
+
"casual": "casual", "随意": "casual", "轻松": "casual",
|
|
237
|
+
"professional": "professional", "专业": "professional"
|
|
238
|
+
}
|
|
239
|
+
for keyword, style in style_map.items():
|
|
240
|
+
if keyword in message.lower():
|
|
241
|
+
params["tutor_style"] = style
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# Extract ratio
|
|
245
|
+
if "ratio" in cmd_name:
|
|
246
|
+
ratio_match = re.search(r"(\d{1,3})\s*(%|percent|百分比)?", message)
|
|
247
|
+
if ratio_match:
|
|
248
|
+
params["oral_written_ratio"] = int(ratio_match.group(1)) / 100.0
|
|
249
|
+
|
|
250
|
+
# Extract error notebook pagination params
|
|
251
|
+
if cmd_name and cmd_name.startswith("errors"):
|
|
252
|
+
# Page number
|
|
253
|
+
page_match = re.search(r"(第\s*)?(\d+)(\s*页|page)", message, re.I)
|
|
254
|
+
if page_match:
|
|
255
|
+
params["page"] = int(page_match.group(2))
|
|
256
|
+
|
|
257
|
+
# Month filter (YYYY-MM)
|
|
258
|
+
month_match = re.search(r"(\d{4}-\d{2})(?!-\d{2})", message)
|
|
259
|
+
if month_match:
|
|
260
|
+
params["month"] = month_match.group(1)
|
|
261
|
+
|
|
262
|
+
# Random count
|
|
263
|
+
random_match = re.search(r"(随机|random)\s*(\d*)", message, re.I)
|
|
264
|
+
if random_match:
|
|
265
|
+
count = int(random_match.group(2)) if random_match.group(2) else 5
|
|
266
|
+
params["random"] = count
|
|
267
|
+
|
|
268
|
+
# Default page for "more" command
|
|
269
|
+
if "more" in cmd_name:
|
|
270
|
+
params["page"] = params.get("page", 2)
|
|
271
|
+
|
|
272
|
+
# Review count (for errors_review command)
|
|
273
|
+
if "review" in cmd_name:
|
|
274
|
+
count_match = re.search(r"(复习|review|练习|practice)\s*(\d*)", message, re.I)
|
|
275
|
+
if count_match and count_match.group(2):
|
|
276
|
+
params["count"] = int(count_match.group(2))
|
|
277
|
+
else:
|
|
278
|
+
params["count"] = 5 # Default 5 questions per session
|
|
279
|
+
|
|
280
|
+
return params
|
|
281
|
+
|
|
282
|
+
def get_command_suggestions(self, context: str = "general") -> list:
|
|
283
|
+
"""
|
|
284
|
+
Get suggested commands based on context.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
context: Context for suggestions (general, after_quiz, morning, etc.)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of suggested command strings
|
|
291
|
+
"""
|
|
292
|
+
suggestions = {
|
|
293
|
+
"general": ["keypoint", "quiz", "stats", "help"],
|
|
294
|
+
"after_quiz": ["stats", "errors", "keypoint"],
|
|
295
|
+
"morning": ["keypoint", "quiz"],
|
|
296
|
+
"evening": ["quiz", "stats"]
|
|
297
|
+
}
|
|
298
|
+
return suggestions.get(context, suggestions["general"])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# CLI interface for testing
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
import argparse
|
|
304
|
+
import json
|
|
305
|
+
|
|
306
|
+
parser = argparse.ArgumentParser(description="Command Parser for eng-lang-tutor")
|
|
307
|
+
parser.add_argument('--message', type=str, help='Message to parse')
|
|
308
|
+
parser.add_argument('--demo', action='store_true', help='Run demo')
|
|
309
|
+
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
|
|
312
|
+
cp = CommandParser()
|
|
313
|
+
|
|
314
|
+
if args.demo:
|
|
315
|
+
test_messages = [
|
|
316
|
+
"start",
|
|
317
|
+
"今天知识点",
|
|
318
|
+
"keypoint today",
|
|
319
|
+
"quiz",
|
|
320
|
+
"我的进度",
|
|
321
|
+
"stats",
|
|
322
|
+
"config",
|
|
323
|
+
"设置 CEFR 为 B2",
|
|
324
|
+
"help",
|
|
325
|
+
"错题本"
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
print("=== Command Parser Demo ===\n")
|
|
329
|
+
for msg in test_messages:
|
|
330
|
+
result = cp._parse_command(msg)
|
|
331
|
+
print(f"Message: {msg}")
|
|
332
|
+
print(f"Result: {json.dumps(result, indent=2)}\n")
|
|
333
|
+
|
|
334
|
+
elif args.message:
|
|
335
|
+
result = cp._parse_command(args.message)
|
|
336
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|