samu commited on
Commit
d08ac14
·
1 Parent(s): f6a6a60

aisqlite library

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. prev_backend_v0/backend/__pycache__/config.cpython-310.pyc +0 -0
  2. prev_backend_v0/backend/__pycache__/config.cpython-312.pyc +0 -0
  3. prev_backend_v0/backend/__pycache__/database.cpython-310.pyc +0 -0
  4. prev_backend_v0/backend/__pycache__/database.cpython-312.pyc +0 -0
  5. prev_backend_v0/backend/__pycache__/main.cpython-310.pyc +0 -0
  6. prev_backend_v0/backend/__pycache__/main.cpython-312.pyc +0 -0
  7. prev_backend_v0/backend/config.py +0 -262
  8. prev_backend_v0/backend/database.py +0 -293
  9. prev_backend_v0/backend/main.py +0 -155
  10. prev_backend_v0/backend/utils/__pycache__/generate_completions.cpython-310.pyc +0 -0
  11. prev_backend_v0/backend/utils/__pycache__/generate_completions.cpython-312.pyc +0 -0
  12. prev_backend_v1/prev_backend_v/__pycache__/config.cpython-310.pyc +0 -0
  13. prev_backend_v1/prev_backend_v/__pycache__/config.cpython-312.pyc +0 -0
  14. prev_backend_v1/prev_backend_v/__pycache__/database.cpython-310.pyc +0 -0
  15. prev_backend_v1/prev_backend_v/__pycache__/database.cpython-312.pyc +0 -0
  16. prev_backend_v1/prev_backend_v/__pycache__/main.cpython-310.pyc +0 -0
  17. prev_backend_v1/prev_backend_v/__pycache__/main.cpython-312.pyc +0 -0
  18. prev_backend_v1/prev_backend_v/config.py +0 -350
  19. prev_backend_v1/prev_backend_v/database.py +0 -293
  20. prev_backend_v1/prev_backend_v/main.py +0 -177
  21. prev_backend_v1/prev_backend_v/utils/__pycache__/generate_completions.cpython-310.pyc +0 -0
  22. prev_backend_v1/prev_backend_v/utils/__pycache__/generate_completions.cpython-312.pyc +0 -0
  23. prev_backend_v1/prev_backend_v/utils/generate_completions.py +0 -107
  24. prev_backend_v2/backend/__pycache__/config.cpython-310.pyc +0 -0
  25. prev_backend_v2/backend/__pycache__/config.cpython-312.pyc +0 -0
  26. prev_backend_v2/backend/__pycache__/database.cpython-310.pyc +0 -0
  27. prev_backend_v2/backend/__pycache__/database.cpython-312.pyc +0 -0
  28. prev_backend_v2/backend/__pycache__/main.cpython-310.pyc +0 -0
  29. prev_backend_v2/backend/__pycache__/main.cpython-312.pyc +0 -0
  30. prev_backend_v2/backend/database.py +0 -293
  31. prev_backend_v2/backend/main.py +0 -189
  32. prev_backend_v2/backend/utils/__pycache__/generate_completions.cpython-310.pyc +0 -0
  33. prev_backend_v2/backend/utils/generate_completions.py +0 -107
  34. prev_backend_v4/backend/__pycache__/config.cpython-311.pyc +0 -0
  35. prev_backend_v4/backend/__pycache__/config.cpython-312.pyc +0 -0
  36. prev_backend_v4/backend/__pycache__/content_generator.cpython-311.pyc +0 -0
  37. prev_backend_v4/backend/__pycache__/content_generator.cpython-312.pyc +0 -0
  38. prev_backend_v4/backend/__pycache__/db.cpython-311.pyc +0 -0
  39. prev_backend_v4/backend/__pycache__/db.cpython-312.pyc +0 -0
  40. prev_backend_v4/backend/__pycache__/db_cache.cpython-311.pyc +0 -0
  41. prev_backend_v4/backend/__pycache__/db_cache.cpython-312.pyc +0 -0
  42. prev_backend_v4/backend/__pycache__/db_init.cpython-311.pyc +0 -0
  43. prev_backend_v4/backend/__pycache__/db_init.cpython-312.pyc +0 -0
  44. prev_backend_v4/backend/__pycache__/main.cpython-311.pyc +0 -0
  45. prev_backend_v4/backend/__pycache__/main.cpython-312.pyc +0 -0
  46. prev_backend_v4/backend/cache.py +29 -0
  47. {prev_backend_v2 → prev_backend_v4}/backend/config.py +8 -13
  48. prev_backend_v4/backend/content_generator.py +295 -0
  49. prev_backend_v4/backend/database_init.py +65 -0
  50. prev_backend_v4/backend/db.py +434 -0
prev_backend_v0/backend/__pycache__/config.cpython-310.pyc DELETED
Binary file (12 kB)
 
prev_backend_v0/backend/__pycache__/config.cpython-312.pyc DELETED
Binary file (14.4 kB)
 
prev_backend_v0/backend/__pycache__/database.cpython-310.pyc DELETED
Binary file (10.1 kB)
 
prev_backend_v0/backend/__pycache__/database.cpython-312.pyc DELETED
Binary file (12.6 kB)
 
prev_backend_v0/backend/__pycache__/main.cpython-310.pyc DELETED
Binary file (3.28 kB)
 
prev_backend_v0/backend/__pycache__/main.cpython-312.pyc DELETED
Binary file (6.71 kB)
 
prev_backend_v0/backend/config.py DELETED
@@ -1,262 +0,0 @@
1
- language_metadata_extraction_prompt = """
2
- You are a language learning assistant. Your task is to analyze the user's input and infer their:
3
- - Native language (use the language of the input as a fallback if unsure)
4
- - Target language (the one they want to learn)
5
- - Proficiency level (beginner, intermediate, or advanced)
6
-
7
- Respond ONLY with a valid JSON object using the following format:
8
-
9
- {
10
- "native_language": "<user's native language>",
11
- "target_language": "<language the user wants to learn>",
12
- "proficiency_level": "<beginner | intermediate | advanced>"
13
- }
14
-
15
- Guidelines:
16
- - If the user's native language is not explicitly stated, assume it's the same as the language used in the query.
17
- - If the target language is mentioned indirectly (e.g. "my Dutch isn't great"), infer that as the target language.
18
- - Make a reasonable guess at proficiency based on clues like "isn't great" → beginner or "I want to improve" → intermediate.
19
- - If you cannot infer something at all, write "unknown".
20
-
21
- Do not include any explanations, comments, or formatting — only valid JSON.
22
- """
23
-
24
- flashcard_mode_instructions = """
25
- # Metadata:
26
- # Native language: {native_language}
27
- # Target language: {target_language}
28
- # Proficiency level: {proficiency}
29
-
30
- You are a highly adaptive vocabulary tutor capable of teaching any language. Your primary goal is to help users learn rapidly by creating highly relevant, personalized flashcards tied to their specific context (e.g., hobbies, work, studies).
31
-
32
- ### Context Format
33
- You will receive a series of messages in the following structure:
34
- [
35
- {"role": "user", "content": "<user input or query>"},
36
- {"role": "assistant", "content": "<flashcards or assistant response>"},
37
- ...
38
- ]
39
- Treat this list as prior conversation history. Use it to:
40
- - Identify the user's learning patterns, interests, and vocabulary already introduced.
41
- - Avoid repeating previously generated flashcards.
42
- - Adjust difficulty based on progression.
43
-
44
- ### Generation Guidelines
45
- When generating a new set of flashcards:
46
- 1. **Use the provided metadata**:
47
- - **Native language**: The language the user is typing in (for definitions).
48
- - **Target language**: The language the user is trying to learn (for words and example sentences).
49
- - **Proficiency level**: Adjust difficulty of words based on the user’s stated proficiency.
50
-
51
- 2. **Avoid repetition**:
52
- - If a word has already been introduced in a previous flashcard, do not repeat it.
53
- - Reference previous assistant responses to build upon previous lessons, ensuring that vocabulary progression is logically consistent.
54
-
55
- 3. **Adjust content based on proficiency**:
56
- - For **beginner** users, use basic, high-frequency vocabulary.
57
- - For **intermediate** users, introduce more complex terms that reflect an expanding knowledge base.
58
- - For **advanced** users, use nuanced or technical terms that align with their expertise and specific context.
59
-
60
- 4. **Domain relevance**:
61
- - Make sure the words and examples are specific to the user’s context (e.g., their profession, hobbies, or field of study).
62
- - Use the latest user query to guide the vocabulary selection and examples. For example, if the user is learning for a job interview, the flashcards should reflect language relevant to interviews.
63
-
64
- ### Flashcard Format
65
- Generate exactly **5 flashcards** as a **valid JSON array**, with each flashcard containing:
66
- - `"word"`: A critical or frequently used word/phrase in the **target language**, tied to the user's domain.
67
- - `"definition"`: A concise, learner-friendly definition in the **base language** (the user’s native language).
68
- - `"example"`: A natural example sentence in the **target language**, demonstrating the word **within the user’s domain**.
69
-
70
- ### Example Query and Expected Output
71
-
72
- #### Example Query:
73
- User: "Flashcards for my hobby: landscape photography in German (intermediate level, base: English)"
74
-
75
- #### Example Output:
76
- ```json
77
- [
78
- {"word": "Belichtung", "definition": "exposure (photography)", "example": "Die richtige Belichtung ist entscheidend für ein gutes Landschaftsfoto."},
79
- {"word": "Stativ", "definition": "tripod", "example": "Bei Langzeitbelichtungen brauchst du ein stabiles Stativ."},
80
- {"word": "Weitwinkelobjektiv", "definition": "wide-angle lens", "example": "Für weite Landschaften benutze ich oft ein Weitwinkelobjektiv."},
81
- {"word": "Goldene Stunde", "definition": "golden hour", "example": "Das Licht während der Goldenen Stunde ist perfekt für dramatische Aufnahmen."},
82
- {"word": "Filter", "definition": "filter (lens filter)", "example": "Ein Polarisationsfilter kann Reflexionen reduzieren und den Himmel betonen."}
83
- ]
84
- """
85
-
86
- exercise_mode_instructions = """
87
- # Metadata:
88
- # Native language: {native_language}
89
- # Target language: {target_language}
90
- # Proficiency level: {proficiency}
91
-
92
- You are a smart, context-aware language exercise generator. Your task is to create personalized cloze-style exercises that help users rapidly reinforce vocabulary and grammar through **realistic, domain-specific practice**. You support any language.
93
-
94
- ### Context Format
95
- You will receive a list of previous messages:
96
- [
97
- {"role": "user", "content": "<user input or query>"},
98
- {"role": "assistant", "content": "<generated exercises>"}
99
- ]
100
- Treat this list as prior conversation history. Use it to:
101
- - Identify the user's learning patterns, interests, and vocabulary already introduced.
102
- - Avoid repeating exercises or vocabulary.
103
- - Ensure progression in complexity or topic coverage.
104
- - Maintain continuity with the user’s learning focus.
105
-
106
- ### Generation Task
107
- When generating a new set of exercises:
108
- 1. **Use the provided metadata**:
109
- - **Native language**: The user’s base language for definitions and understanding.
110
- - **Target language**: The language the user is learning for both exercises and answers.
111
- - **Proficiency level**: Adjust the complexity of the exercises based on the user's proficiency (beginner, intermediate, advanced).
112
-
113
- 2. **Domain relevance**:
114
- - Focus on the **domain of interest** (e.g., work, hobby, study area).
115
- - Use context from previous queries to tailor the exercises, ensuring they are practical and connected to the user’s personal or professional life.
116
-
117
- 3. **Avoid repetition**:
118
- - Ensure that previously used vocabulary or sentence structures are not repeated.
119
- - Each new exercise should introduce new vocabulary or grammar concepts based on the user’s progression.
120
-
121
- 4. **Adjust difficulty**:
122
- - For **beginner** users, keep the sentences simple and focus on high-frequency vocabulary.
123
- - For **intermediate** users, incorporate slightly more complex structures and vocabulary.
124
- - For **advanced** users, use more nuanced grammar and specialized vocabulary relevant to their domain.
125
-
126
- ### Output Format
127
- Produce exactly **5 cloze-style exercises** as a **valid JSON array**, with each item containing:
128
- - `"sentence"`: A sentence in the **target language** that includes a blank `'___'` for a missing vocabulary word or grammar element. The sentence should be relevant to the user’s domain of interest.
129
- - `"answer"`: The correct word or phrase to fill in the blank.
130
- - `"choices"`: A list of 3 plausible options (including the correct answer) in the target language. Distractors should be believable but clearly incorrect in context.
131
-
132
- ### Example Query and Expected Output
133
-
134
- #### Example Query:
135
- User: "Beginner French exercises about my work in marketing (base: English)"
136
-
137
- #### Expected Output:
138
- ```json
139
- [
140
- {"sentence": "Nous devons lancer la nouvelle ___ le mois prochain.", "answer": "campagne", "choices": ["campagne", "produit", "réunion"]},
141
- {"sentence": "Quel est le ___ principal de ce projet ?", "answer": "objectif", "choices": ["client", "objectif", "budget"]},
142
- {"sentence": "Il faut analyser le ___ avant de prendre une décision.", "answer": "marché", "choices": ["marché", "bureau", "téléphone"]},
143
- {"sentence": "Elle prépare une ___ pour les clients.", "answer": "présentation", "choices": ["facture", "présentation", "publicité"]},
144
- {"sentence": "Nous utilisons les ___ sociaux pour la promotion.", "answer": "réseaux", "choices": ["médias", "réseaux", "journaux"]}
145
- ]
146
- """
147
-
148
- simulation_mode_instructions = """
149
- # Metadata:
150
- # Native language: {native_language}
151
- # Target language: {target_language}
152
- # Proficiency level: {proficiency}
153
-
154
- You are a **creative, context-aware storytelling engine**. Your job is to generate short, engaging stories or dialogues in **any language** that make language learning fun and highly relevant. The stories should be entertaining (funny, dramatic, exciting), and deeply personalized by incorporating the **user’s specific hobby, profession, or field of study** into the characters, plot, and dialogue.
155
-
156
- ### Context Format
157
- You will receive a list of prior messages:
158
- [
159
- {"role": "user", "content": "<user input>"},
160
- {"role": "assistant", "content": "<last generated story>"}
161
- ]
162
- Treat this list as prior conversation history. Use it to:
163
- - Avoid repeating ideas, themes, or jokes from previous responses.
164
- - Build on past tone, vocabulary, or characters if appropriate.
165
- - Adjust story complexity based on past user proficiency or feedback cues.
166
-
167
- ### Story Generation Task
168
- From the latest user message:
169
- 1. **Use the provided metadata**:
170
- - **Native language**: The user’s base language for understanding.
171
- - **Target language**: The language the user is learning.
172
- - **Proficiency level**: Adjust the complexity of the story or dialogue based on the user’s proficiency level.
173
-
174
- 2. **Domain relevance**:
175
- - Focus on the **user's domain of interest** (e.g., work, hobby, field of study).
176
- - Use **realistic terminology or scenarios** related to their interests to make the story engaging and practical.
177
-
178
- 3. **Adjust story complexity**:
179
- - For **beginner** learners, keep sentences simple and direct with basic vocabulary and grammar.
180
- - For **intermediate** learners, use natural dialogue, simple narrative structures, and introduce moderately challenging vocabulary.
181
- - For **advanced** learners, incorporate idiomatic expressions, complex sentence structures, and domain-specific language.
182
-
183
- 4. **Avoid repetition**:
184
- - Ensure that new stories or dialogues bring fresh content and characters. Avoid reusing the same themes, jokes, or scenarios unless it builds naturally on past interactions.
185
-
186
- 5. **Engage with the user’s tone and interests**:
187
- - If the user is passionate about a specific topic (e.g., cooking, space exploration, or law), integrate that into the story. If the user likes humor, use a fun tone; for drama or excitement, make the story engaging with conflict or high stakes.
188
-
189
- ### Output Format
190
- Return a valid **JSON object** with the following structure:
191
- - `"title"`: An engaging title in the **native language**.
192
- - `"setting"`: A short setup in the **native language** explaining the story’s background, tailored to the user’s interest.
193
- - `"content"`: A list of **6–10 segments**, each containing:
194
- - `"speaker"`: Name or role of the speaker in the **native language** (e.g., "Narrator", "Professor Lee", "The Engineer").
195
- - `"target_language_text"`: Sentence in the **target language**.
196
- - `"phonetics"`: Standardized phonetic transcription (IPA, Pinyin, etc.) if applicable and helpful. Omit if unavailable or not useful.
197
- - `"base_language_translation"`: Simple translation of the sentence in the **native language**.
198
-
199
- ### Personalization Rules
200
- - Base the humor, conflict, and events directly on the user’s interest. For example:
201
- - If the user loves space, create an exciting stargazing story.
202
- - If they study law, create a courtroom dialogue with legal terms.
203
- - If they’re into cooking, make the story about a cooking adventure.
204
- - Include real terminology or realistic situations from the domain to make learning useful and immersive.
205
- - Adjust the tone and vocabulary complexity based on user proficiency level (beginner = simple, intermediate = natural, advanced = idiomatic).
206
- - Keep the pacing tight — avoid overly long narrations or explanations.
207
-
208
- ### Output Instructions
209
- Return only the final **JSON object**. Do not include:
210
- - Explanations
211
- - Notes
212
- - Comments
213
- - Markdown formatting
214
-
215
- ### Example User Input
216
- "Funny story for intermediate French learner about cooking hobby (base: English)"
217
-
218
- ### Example Output (French)
219
- ```json
220
- {
221
- "title": "La Panique de la Paella",
222
- "setting": "Pierre essaie d'impressionner ses amis en cuisinant une paella espagnole authentique pour la première fois.",
223
- "content": [
224
- {
225
- "speaker": "Narrateur",
226
- "target_language_text": "Pierre regarda la recette de paella. Cela semblait facile.",
227
- "phonetics": "pjeʁ ʁəɡaʁda la ʁesɛt də paɛʎa. sə.la sɛ̃blɛ ɛ.fa.sil",
228
- "base_language_translation": "Pierre looked at the paella recipe. It seemed easy."
229
- },
230
- {
231
- "speaker": "Pierre",
232
- "target_language_text": "Il me faut du safran! Où est le safran?",
233
- "phonetics": "il mə fo dy sa.fʁɑ̃! u ɛ lə sa.fʁɑ̃",
234
- "base_language_translation": "I need saffron! Where is the saffron?"
235
- },
236
- {
237
- "speaker": "Narrateur",
238
- "target_language_text": "Pierre fouilla le placard, mais il ne trouva pas de safran.",
239
- "phonetics": "pjeʁ fwi.jɑ lə pla.kɑʁ, mɛ il nə tʁu.va pa də sa.fʁɑ̃",
240
- "base_language_translation": "Pierre searched the cupboard, but he couldn’t find any saffron."
241
- },
242
- {
243
- "speaker": "Pierre",
244
- "target_language_text": "Qu'est-ce que je vais faire maintenant ?",
245
- "phonetics": "kɛs.kə ʒə vɛ fɛʁ mɛ̃tə.nɑ̃?",
246
- "base_language_translation": "What am I going to do now?"
247
- },
248
- {
249
- "speaker": "Narrateur",
250
- "target_language_text": "Finalement, Pierre décida de remplacer le safran par du curcuma.",
251
- "phonetics": "fi.nal.mɑ̃ pjeʁ de.si.da də ʁɑ̃.pla.sə lə sa.fʁɑ̃ paʁ dy kyʁ.ky.ma",
252
- "base_language_translation": "Finally, Pierre decided to replace the saffron with turmeric."
253
- },
254
- {
255
- "speaker": "Pierre",
256
- "target_language_text": "C'est presque pareil, non ?",
257
- "phonetics": "sɛ pʁɛs.kə paʁɛj, nɔ̃?",
258
- "base_language_translation": "It's almost the same, right?"
259
- }
260
- ]
261
- }
262
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v0/backend/database.py DELETED
@@ -1,293 +0,0 @@
1
- import psycopg2
2
- import os
3
- from psycopg2 import sql
4
- from dotenv import load_dotenv
5
-
6
- load_dotenv()
7
-
8
- # Database Configuration from environment variables
9
- DB_NAME = os.getenv("POSTGRES_DB", "linguaai")
10
- DB_USER = os.getenv("POSTGRES_USER", "linguaai_user")
11
- DB_PASSWORD = os.getenv("POSTGRES_PASSWORD", "LinguaAI1008")
12
- DB_HOST = os.getenv("DB_HOST", "localhost")
13
- DB_PORT = os.getenv("DB_PORT", "5432")
14
-
15
- # SQL Schema Definition
16
- SCHEMA_SQL = """
17
- -- Drop existing objects if they exist
18
- -- Note: Some drops below might be for tables not defined in this specific script.
19
- DROP TABLE IF EXISTS user_activity_progress CASCADE;
20
- DROP TABLE IF EXISTS activities CASCADE;
21
- DROP TABLE IF EXISTS weekly_modules CASCADE;
22
- DROP TABLE IF EXISTS curriculums CASCADE;
23
- DROP TABLE IF EXISTS generated_flashcards CASCADE;
24
- DROP TABLE IF EXISTS flashcard_sets CASCADE; -- Corrected name
25
- DROP TABLE IF EXISTS generated_exercises CASCADE;
26
- DROP TABLE IF EXISTS exercise_sets CASCADE; -- Corrected name
27
- DROP TABLE IF EXISTS simulations CASCADE; -- Corrected name
28
- DROP TABLE IF EXISTS users CASCADE;
29
- DROP TYPE IF EXISTS activity_status CASCADE;
30
-
31
- -- Table `users`
32
- CREATE TABLE users (
33
- user_id SERIAL PRIMARY KEY,
34
- username VARCHAR(50) UNIQUE NOT NULL,
35
- email VARCHAR(100) UNIQUE NOT NULL,
36
- password_hash VARCHAR(255) NOT NULL,
37
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
38
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
39
- );
40
-
41
- -- Trigger function (remains the same)
42
- CREATE OR REPLACE FUNCTION update_updated_at_column()
43
- RETURNS TRIGGER AS $$
44
- BEGIN
45
- NEW.updated_at = now();
46
- RETURN NEW;
47
- END;
48
- $$ language 'plpgsql';
49
-
50
- -- Trigger for users (remains the same)
51
- CREATE TRIGGER users_update_updated_at
52
- BEFORE UPDATE ON users
53
- FOR EACH ROW
54
- EXECUTE FUNCTION update_updated_at_column();
55
-
56
-
57
- -- ============================================
58
- -- Tables for Generated Content (Flashcards)
59
- -- ============================================
60
-
61
- -- Table `flashcard_sets` (Represents one request/query)
62
- CREATE TABLE flashcard_sets (
63
- id SERIAL PRIMARY KEY,
64
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
65
- query TEXT NOT NULL,
66
- flashcards JSONB NOT NULL, -- Stores an array of 5 flashcards
67
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
69
- );
70
-
71
- CREATE INDEX idx_flashcard_set_user ON flashcard_sets(user_id);
72
-
73
- -- Corrected Trigger definition for flashcard_sets
74
- CREATE TRIGGER flashcard_sets_update_updated_at -- Renamed trigger
75
- BEFORE UPDATE ON flashcard_sets -- Corrected table name
76
- FOR EACH ROW
77
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
78
-
79
- -- Table `generated_flashcards` (Individual flashcards within a set)
80
- CREATE TABLE generated_flashcards (
81
- flashcard_id SERIAL PRIMARY KEY,
82
- set_id INT NOT NULL REFERENCES flashcard_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
83
- word TEXT NOT NULL,
84
- definition TEXT NOT NULL,
85
- example TEXT, -- Example might be optional
86
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
87
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
88
- );
89
-
90
- CREATE INDEX idx_flashcard_set ON generated_flashcards(set_id);
91
-
92
- -- Trigger for generated_flashcards (remains the same)
93
- CREATE TRIGGER generated_flashcards_update_updated_at
94
- BEFORE UPDATE ON generated_flashcards
95
- FOR EACH ROW
96
- EXECUTE FUNCTION update_updated_at_column();
97
-
98
-
99
- -- ============================================
100
- -- Tables for Generated Content (Exercises)
101
- -- ============================================
102
-
103
- -- Table `exercise_sets` (Represents one request/query) -- Corrected comment
104
- CREATE TABLE exercise_sets (
105
- id SERIAL PRIMARY KEY,
106
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
107
- query TEXT NOT NULL,
108
- exercises JSONB NOT NULL, -- Array of 5 exercises
109
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
111
- );
112
-
113
- CREATE INDEX idx_exercise_set_user ON exercise_sets(user_id); -- Corrected table name (was already correct but double-checked)
114
-
115
- -- Corrected Trigger definition for exercise_sets
116
- CREATE TRIGGER exercise_sets_update_updated_at -- Renamed trigger
117
- BEFORE UPDATE ON exercise_sets -- Corrected table name
118
- FOR EACH ROW
119
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
120
-
121
- -- Table `generated_exercises` (Individual exercises within a set)
122
- CREATE TABLE generated_exercises (
123
- exercise_id SERIAL PRIMARY KEY,
124
- set_id INT NOT NULL REFERENCES exercise_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
125
- sentence TEXT NOT NULL,
126
- answer TEXT NOT NULL,
127
- choices JSONB NOT NULL, -- Storing the array of choices
128
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
129
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
130
- );
131
-
132
- CREATE INDEX idx_exercise_set ON generated_exercises(set_id);
133
-
134
- -- Trigger for generated_exercises (remains the same)
135
- CREATE TRIGGER generated_exercises_update_updated_at
136
- BEFORE UPDATE ON generated_exercises
137
- FOR EACH ROW
138
- EXECUTE FUNCTION update_updated_at_column();
139
-
140
-
141
- -- ============================================
142
- -- Table for Generated Content (Simulations)
143
- -- ============================================
144
-
145
- -- Table `simulations` (Represents one simulation request/result) -- Corrected comment
146
- CREATE TABLE simulations (
147
- id SERIAL PRIMARY KEY,
148
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
149
- query TEXT NOT NULL,
150
- scenario TEXT NOT NULL,
151
- dialog JSONB NOT NULL, -- Array of turns with 'role', 'chinese', 'pinyin', 'english'
152
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
154
- );
155
-
156
- CREATE INDEX idx_simulation_user ON simulations(user_id); -- Corrected table name
157
-
158
- -- Corrected Trigger definition for simulations
159
- CREATE TRIGGER simulations_update_updated_at -- Renamed trigger
160
- BEFORE UPDATE ON simulations -- Corrected table name
161
- FOR EACH ROW
162
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
163
- """
164
-
165
- def get_db_connection():
166
- """Get a synchronous database connection."""
167
- try:
168
- conn = psycopg2.connect(
169
- dbname=DB_NAME,
170
- user=DB_USER,
171
- password=DB_PASSWORD,
172
- host=DB_HOST,
173
- port=DB_PORT
174
- )
175
- return conn
176
- except psycopg2.Error as e:
177
- print(f"Database connection error: {e}")
178
- raise
179
-
180
- def reset_sequences():
181
- """Generate SQL to reset all sequences (auto-incrementing IDs) to 1."""
182
- sequences_sql = """
183
- SELECT 'ALTER SEQUENCE ' || sequence_name || ' RESTART WITH 1;'
184
- FROM information_schema.sequences
185
- WHERE sequence_schema = 'public';
186
- """
187
- return sequences_sql
188
-
189
- def reset_database(confirm=True):
190
- """Reset the database by dropping all tables and recreating them."""
191
- if confirm:
192
- user_confirm = input("WARNING: This will DELETE ALL DATA. Type 'yes' to proceed: ")
193
- if user_confirm.lower() != 'yes':
194
- print("Database reset cancelled.")
195
- return
196
-
197
- conn = None
198
- try:
199
- conn = get_db_connection()
200
- conn.autocommit = False
201
- print("Database connection established.")
202
-
203
- with conn.cursor() as cur:
204
- print("Dropping and recreating schema...")
205
- # Execute the main schema SQL (includes drops)
206
- cur.execute(SCHEMA_SQL)
207
- print("Schema recreated successfully.")
208
-
209
- # Generate and execute sequence reset SQL
210
- print("Resetting sequences...")
211
- reset_sql_query = reset_sequences()
212
- cur.execute(reset_sql_query)
213
- reset_commands = cur.fetchall()
214
- for command in reset_commands:
215
- cur.execute(command[0])
216
- print("Sequences reset successfully.")
217
-
218
- conn.commit()
219
- print("Database reset complete.")
220
-
221
- except psycopg2.Error as e:
222
- print(f"Database error during reset: {e}")
223
- if conn:
224
- conn.rollback()
225
- print("Transaction rolled back.")
226
- except Exception as e:
227
- print(f"An unexpected error occurred during reset: {e}")
228
- if conn:
229
- conn.rollback()
230
- finally:
231
- if conn:
232
- conn.close()
233
- print("Database connection closed.")
234
-
235
- def setup_database(confirm=True):
236
- """Set up the database schema if tables do not exist."""
237
- if confirm:
238
- user_confirm = input("Do you want to set up the database? Type 'yes' to proceed: ")
239
- if user_confirm.lower() != 'yes':
240
- print("Database setup cancelled.")
241
- return
242
-
243
- conn = None
244
- try:
245
- conn = get_db_connection()
246
- conn.autocommit = False
247
- print("Database connection established.")
248
-
249
- with conn.cursor() as cur:
250
- print("Checking if tables exist...")
251
- cur.execute("""
252
- SELECT EXISTS (
253
- SELECT FROM information_schema.tables
254
- WHERE table_schema = 'public'
255
- AND table_name = 'users'
256
- );
257
- """)
258
- tables_exist = cur.fetchone()[0]
259
-
260
- if tables_exist:
261
- print("Tables already exist. Use reset_database() to reset the database or run setup with confirm=False.")
262
- conn.rollback() # Rollback as no changes should be made
263
- return
264
-
265
- print("Creating schema...")
266
- cur.execute(SCHEMA_SQL)
267
- print("Schema created successfully.")
268
-
269
- conn.commit()
270
- print("Database setup complete.")
271
-
272
- except psycopg2.Error as e:
273
- print(f"Database error during setup: {e}")
274
- if conn:
275
- conn.rollback()
276
- print("Transaction rolled back.")
277
- except Exception as e:
278
- print(f"An unexpected error occurred during setup: {e}")
279
- if conn:
280
- conn.rollback()
281
- finally:
282
- if conn:
283
- conn.close()
284
- print("Database connection closed.")
285
-
286
- if __name__ == "__main__":
287
- action = input("Enter 'setup' to setup database or 'reset' to reset database: ").lower()
288
- if action == 'reset':
289
- reset_database()
290
- elif action == 'setup':
291
- setup_database()
292
- else:
293
- print("Invalid action. Use 'setup' or 'reset'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v0/backend/main.py DELETED
@@ -1,155 +0,0 @@
1
- from fastapi import FastAPI, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from backend.utils import generate_completions
6
- from backend import config
7
- from backend.database import get_db_connection
8
- import psycopg2
9
- from psycopg2.extras import RealDictCursor
10
- from typing import Union, List, Literal, Optional
11
- import logging
12
- import json
13
-
14
- logging.basicConfig(level=logging.INFO)
15
-
16
- app = FastAPI()
17
-
18
- # Add CORS middleware
19
- app.add_middleware(
20
- CORSMiddleware,
21
- allow_origins=["*"], # Allows all origins
22
- allow_credentials=True,
23
- allow_methods=["*"], # Allows all methods
24
- allow_headers=["*"], # Allows all headers
25
- )
26
-
27
- # Dependency to get database connection
28
- async def get_db():
29
- conn = await get_db_connection()
30
- try:
31
- yield conn
32
- finally:
33
- conn.close()
34
-
35
- # class GenerationRequest(BaseModel):
36
- # user_id: int
37
- # query: str
38
-
39
- class Message(BaseModel):
40
- role: Literal["user", "assistant"]
41
- content: str
42
-
43
- class GenerationRequest(BaseModel):
44
- user_id: int
45
- query: Union[str, List[Message]]
46
-
47
- class MetadataRequest(BaseModel):
48
- query: str
49
-
50
- # Global metadata variables
51
- native_language: Optional[str] = None
52
- target_language: Optional[str] = None
53
- proficiency: Optional[str] = None
54
-
55
- @app.get("/")
56
- async def root():
57
- return {"message": "Welcome to the AI Learning Assistant API!"}
58
-
59
- @app.post("/extract/metadata")
60
- async def extract_metadata(data: MetadataRequest):
61
- try:
62
- response_str = await generate_completions.get_completions(
63
- data.query,
64
- config.language_metadata_extraction_prompt
65
- )
66
- metadata_dict = json.loads(response_str)
67
- # Update globals for other endpoints
68
- globals()['native_language'] = metadata_dict.get('native_language', 'unknown')
69
- globals()['target_language'] = metadata_dict.get('target_language', 'unknown')
70
- globals()['proficiency'] = metadata_dict.get('proficiency_level', 'unknown')
71
- return JSONResponse(
72
- content={
73
- "data": metadata_dict,
74
- "type": "language_metadata",
75
- "status": "success"
76
- },
77
- status_code=200
78
- )
79
- except Exception as e:
80
- raise HTTPException(status_code=500, detail=str(e))
81
-
82
- @app.post("/generate/flashcards")
83
- async def generate_flashcards(data: GenerationRequest):
84
- try:
85
- # Use previously extracted metadata
86
- instructions = (
87
- config.flashcard_mode_instructions
88
- .replace("{native_language}", native_language or "unknown")
89
- .replace("{target_language}", target_language or "unknown")
90
- .replace("{proficiency}", proficiency or "unknown")
91
- )
92
- response = await generate_completions.get_completions(
93
- data.query,
94
- instructions
95
- )
96
- return JSONResponse(
97
- content={
98
- "data": response,
99
- "type": "flashcards",
100
- "status": "success"
101
- },
102
- status_code=200
103
- )
104
- except Exception as e:
105
- raise HTTPException(status_code=500, detail=str(e))
106
-
107
- @app.post("/generate/exercises")
108
- async def generate_exercises(data: GenerationRequest):
109
- try:
110
- # Use previously extracted metadata
111
- instructions = (
112
- config.exercise_mode_instructions
113
- .replace("{native_language}", native_language or "unknown")
114
- .replace("{target_language}", target_language or "unknown")
115
- .replace("{proficiency}", proficiency or "unknown")
116
- )
117
- response = await generate_completions.get_completions(
118
- data.query,
119
- instructions
120
- )
121
- return JSONResponse(
122
- content={
123
- "data": response,
124
- "type": "exercises",
125
- "status": "success"
126
- },
127
- status_code=200
128
- )
129
- except Exception as e:
130
- raise HTTPException(status_code=500, detail=str(e))
131
-
132
- @app.post("/generate/simulation")
133
- async def generate_simulation(data: GenerationRequest):
134
- try:
135
- # Use previously extracted metadata
136
- instructions = (
137
- config.simulation_mode_instructions
138
- .replace("{native_language}", native_language or "unknown")
139
- .replace("{target_language}", target_language or "unknown")
140
- .replace("{proficiency}", proficiency or "unknown")
141
- )
142
- response = await generate_completions.get_completions(
143
- data.query,
144
- instructions
145
- )
146
- return JSONResponse(
147
- content={
148
- "data": response,
149
- "type": "simulation",
150
- "status": "success"
151
- },
152
- status_code=200
153
- )
154
- except Exception as e:
155
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v0/backend/utils/__pycache__/generate_completions.cpython-310.pyc DELETED
Binary file (2.55 kB)
 
prev_backend_v0/backend/utils/__pycache__/generate_completions.cpython-312.pyc DELETED
Binary file (3.69 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/config.cpython-310.pyc DELETED
Binary file (12 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/config.cpython-312.pyc DELETED
Binary file (17.2 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/database.cpython-310.pyc DELETED
Binary file (10.1 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/database.cpython-312.pyc DELETED
Binary file (12.6 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/main.cpython-310.pyc DELETED
Binary file (3.28 kB)
 
prev_backend_v1/prev_backend_v/__pycache__/main.cpython-312.pyc DELETED
Binary file (7.76 kB)
 
prev_backend_v1/prev_backend_v/config.py DELETED
@@ -1,350 +0,0 @@
1
- language_metadata_extraction_prompt = """
2
- You are a language learning assistant. Your task is to analyze the user's input and infer their:
3
- - Native language (use the language of the input as a fallback if unsure)
4
- - Target language (the one they want to learn)
5
- - Proficiency level (beginner, intermediate, or advanced)
6
-
7
- Respond ONLY with a valid JSON object using the following format:
8
-
9
- {
10
- "native_language": "<user's native language>",
11
- "target_language": "<language the user wants to learn>",
12
- "proficiency_level": "<beginner | intermediate | advanced>"
13
- }
14
-
15
- Guidelines:
16
- - Prioritize explicit statements about the native language (e.g., 'I’m a native Spanish speaker') over the language of the input. If no explicit statement is provided, assume the language of the input. If still unsure, default to 'english'.
17
- - Infer the target language from explicit mentions (e.g., 'I want to learn French') or indirect clues (e.g., 'My Dutch isn’t great'). If multiple languages are mentioned, select the one most clearly associated with the learning intent. If ambiguous or no information is available, default to 'english'.
18
- - Infer proficiency level based on clues:
19
- - Beginner: 'isn’t great', 'just starting', 'learning the basics', 'new to', 'struggling with'
20
- - Intermediate: 'want to improve', 'can hold basic conversations', 'okay at', 'decent at', 'some knowledge'
21
- - Advanced: 'fluent', 'can read complex texts', 'almost native', 'very comfortable', 'proficient'
22
- - If no clues are present, default to 'beginner'.
23
- - Use full language names in lowercase English (e.g., 'english', 'spanish', 'french').
24
- - The default to 'english' for native_language and target_language assumes an English-majority context; adjust defaults for other regions if needed. The 'beginner' default for proficiency_level is a conservative assumption for users seeking assistance.
25
-
26
- Examples:
27
- - Input: 'Hi, my Dutch isn’t great.' → {"native_language": "english", "target_language": "dutch", "proficiency_level": "beginner"}
28
- - Input: 'Soy español y quiero aprender inglés.' → {"native_language": "spanish", "target_language": "english", "proficiency_level": "beginner"}
29
- - Input: 'I’m a native French speaker learning German and can hold basic conversations.' → {"native_language": "french", "target_language": "german", "proficiency_level": "intermediate"}
30
- - Input: 'Help me with language learning.' → {"native_language": "english", "target_language": "english", "proficiency_level": "beginner"}
31
- - Input: 'I can read books in Italian but want to get better.' → {"native_language": "english", "target_language": "italian", "proficiency_level": "intermediate"}
32
- - Input: 'I’m fluent in Portuguese.' → {"native_language": "english", "target_language": "portuguese", "proficiency_level": "advanced"}
33
-
34
- Do not include any explanations, comments, or formatting — only valid JSON.
35
- """
36
-
37
- curriculum_instructions = """
38
- # Metadata:
39
- # Native language: {native_language}
40
- # Target language: {target_language}
41
- # Proficiency level: {proficiency}
42
-
43
- You are an AI-powered language learning assistant tasked with generating a tailored curriculum based on the user’s metadata. Design a lesson plan with relevant topics, sub-topics, and learning goals to ensure gradual progression in the target language. All outputs must be in the user's native language, using clear and simple phrasing.
44
-
45
- ### Instructions:
46
- 1. **Select the Lesson Topic (Main Focus):**
47
- - Choose a broad topic based on the user’s target language, proficiency, and inferred interests (e.g., business, travel, daily conversations). If interests are unknown, default to "Daily Conversations."
48
- - Adjust complexity to proficiency:
49
- - Beginner: Basic vocabulary and phrases.
50
- - Intermediate: Conversational skills and grammar.
51
- - Advanced: Specialized vocabulary and nuances.
52
-
53
- 2. **Break Down the Topic into Sub-topics (3-7 recommended):**
54
- - Divide the topic into sub-topics that build progressively, from foundational to advanced skills. Include cultural context where relevant (e.g., etiquette in the target language).
55
- - Example for "Business Vocabulary":
56
- - Sub-topic 1: Greeting colleagues (basic).
57
- - Sub-topic 2: Introducing yourself (intermediate).
58
- - Sub-topic 3: Discussing projects (advanced).
59
-
60
- 3. **Define Measurable Learning Goals for Each Sub-topic:**
61
- - Specify clear, measurable outcomes using action verbs (e.g., "Use," "Explain"). Align goals with proficiency and practical use.
62
- - Example: "Use three professional phrases to introduce yourself."
63
-
64
- ### Output Format:
65
- Return a JSON object with:
66
- - `"lesson_topic"`: Main focus in the user's native language.
67
- - `"sub_topics"`: List of sub-topics, each with:
68
- - `"sub_topic"`: Title in the user's native language.
69
- - `"learning_goals"`: List of measurable goals in the user's native language.
70
-
71
- **Example Output:**
72
- ```json
73
- {
74
- "lesson_topic": "Business Vocabulary",
75
- "sub_topics": [
76
- {
77
- "sub_topic": "Greeting colleagues",
78
- "learning_goals": [
79
- "Use two common greetings in a workplace",
80
- "Respond politely to a greeting"
81
- ]
82
- },
83
- {
84
- "sub_topic": "Introducing yourself professionally",
85
- "learning_goals": [
86
- "Introduce yourself with three professional phrases",
87
- "State your job role clearly"
88
- ]
89
- }
90
- ]
91
- }
92
- """
93
- flashcard_mode_instructions = """
94
- # Metadata:
95
- # Native language: {native_language}
96
- # Target language: {target_language}
97
- # Proficiency level: {proficiency}
98
-
99
- You are a highly adaptive vocabulary tutor capable of teaching any language. Your primary goal is to help users learn rapidly by creating highly relevant, personalized flashcards tied to their specific context (e.g., hobbies, work, studies).
100
-
101
- ### Context Format
102
- You will receive a series of messages in the following structure:
103
- [
104
- {"role": "user", "content": "<user input or query>"},
105
- {"role": "assistant", "content": "<flashcards or assistant response>"},
106
- ...
107
- ]
108
- Treat this list as prior conversation history. Use it to:
109
- - Track the user's learning progression and incrementally increase difficulty over time.
110
- - Identify recurring interests or themes (e.g., photography terms) to focus vocabulary.
111
- - Avoid repeating words or concepts from prior flashcards unless requested.
112
- - Incorporate user feedback or corrections to refine future sets.
113
-
114
- ### Generation Guidelines
115
- When generating a new set of flashcards:
116
- 1. **Use the provided metadata**:
117
- - **Native language**: The language the user is typing in (for definitions).
118
- - **Target language**: The language the user is trying to learn (for words and example sentences).
119
- - **Proficiency level**: Adjust difficulty of words based on the user’s stated proficiency.
120
-
121
- 2. **Avoid repetition**:
122
- - If a word has already been introduced in a previous flashcard, do not repeat it unless explicitly requested.
123
- - Reference previous assistant responses to build upon prior lessons, ensuring logical vocabulary progression.
124
-
125
- 3. **Adjust content based on proficiency**:
126
- - **Beginner**: Use high-frequency words and simple sentence structures (e.g., basic greetings, everyday objects).
127
- - Example: "Hallo" - "Hello" (German-English).
128
- - **Intermediate**: Introduce more complex vocabulary and compound sentences (e.g., common phrases, descriptive language).
129
- - Example: "Ich fotografiere gerne" - "I like to take photos" (German-English).
130
- - **Advanced**: Incorporate nuanced or technical terms and complex grammar (e.g., idiomatic expressions, field-specific jargon).
131
- - Example: "Langzeitbelichtung" - "long exposure" (German-English).
132
-
133
- 4. **Domain relevance**:
134
- - Ensure words and examples are specific to the user’s context (e.g., profession, hobbies).
135
- - If the context is unclear or broad (e.g., "hobbies"), ask a follow-up question (e.g., "What specific hobby are you interested in?") to tailor the flashcards effectively.
136
-
137
- 5. **Handle edge cases**:
138
- - For users with multiple domains (e.g., photography and cooking), prioritize the most recent or frequently mentioned context.
139
- - If the user’s proficiency evolves (e.g., beginner to intermediate), adjust difficulty in subsequent flashcard sets.
140
-
141
- ### Flashcard Format
142
- Generate exactly **5 flashcards** as a **valid JSON array**, with each flashcard containing:
143
- - `"word"`: A critical or frequently used word/phrase in the **target language**, tied to the user's domain.
144
- - `"definition"`: A concise, learner-friendly definition in the **native language**.
145
- - `"example"`: A practical, natural sentence in the **target language** that demonstrates the word in a context directly relevant to the user’s domain (e.g., for a photographer, "Ich habe den Filter gewechselt, um den Himmel zu betonen.").
146
-
147
- ### Example Query and Expected Output
148
-
149
- #### Example Query:
150
- User: "Flashcards for my hobby: landscape photography in German (intermediate level, native: English)"
151
-
152
- #### Example Output:
153
- ```json
154
- [
155
- {"word": "Belichtung", "definition": "exposure (photography)", "example": "Die richtige Belichtung ist entscheidend für ein gutes Landschaftsfoto."},
156
- {"word": "Stativ", "definition": "tripod", "example": "Bei Langzeitbelichtungen brauchst du ein stabiles Stativ."},
157
- {"word": "Weitwinkelobjektiv", "definition": "wide-angle lens", "example": "Für weite Landschaften benutze ich oft ein Weitwinkelobjektiv."},
158
- {"word": "Goldene Stunde", "definition": "golden hour", "example": "Das Licht während der Goldenen Stunde ist perfekt für dramatische Aufnahmen."},
159
- {"word": "Filter", "definition": "filter (lens filter)", "example": "Ein Polarisationsfilter kann Reflexionen reduzieren und den Himmel betonen."}
160
- ]
161
- """
162
-
163
- exercise_mode_instructions = """
164
- # Metadata:
165
- # Native language: {native_language}
166
- # Target language: {target_language}
167
- # Proficiency level: {proficiency}
168
-
169
- You are a smart, context-aware language exercise generator. Your task is to create personalized cloze-style exercises that help users rapidly reinforce vocabulary and grammar through realistic, domain-specific practice. You support any language.
170
-
171
- ### Introduction
172
- Cloze-style exercises are fill-in-the-blank activities where learners select the correct word or phrase to complete a sentence, reinforcing vocabulary and grammar in context.
173
-
174
- ### Context Format
175
- You will receive a list of previous messages:
176
- [
177
- {"role": "user", "content": "<user input or query>"},
178
- {"role": "assistant", "content": "<generated exercises>"}
179
- ]
180
- Treat this list as prior conversation history. Use it to:
181
- - Track previously introduced vocabulary and grammar to introduce new concepts.
182
- - Identify recurring interests (e.g., marketing) to refine domain focus.
183
- - Avoid repeating sentences, words, or structures unless intentional for reinforcement.
184
- - Adjust difficulty based on past exercises to ensure progression (e.g., from simple nouns to compound phrases).
185
-
186
- ### Generation Task
187
- When generating a new set of exercises:
188
- 1. **Use the provided metadata**:
189
- - **Native language**: The user’s base language for definitions and understanding.
190
- - **Target language**: The language the user is learning for both exercises and answers.
191
- - **Proficiency level**: Adjust the complexity of the exercises based on the user's proficiency.
192
-
193
- 2. **Domain relevance**:
194
- - Focus on the user’s specified domain (e.g., work, hobby, study area).
195
- - If the domain is vague (e.g., "work"), seek clarification (e.g., "What aspect of your work?") to ensure relevance.
196
- - Use realistic scenarios tied to the domain for practical application.
197
-
198
- 3. **Avoid repetition**:
199
- - Ensure previously used vocabulary or sentence structures are not repeated unless requested.
200
- - Each new exercise should introduce new vocabulary or grammar concepts based on the user’s progression.
201
-
202
- 4. **Adjust difficulty**:
203
- - **Beginner**: Use short, simple sentences with high-frequency vocabulary and basic grammar (e.g., "Je suis ___." - "I am ___").
204
- - **Intermediate**: Include compound sentences with moderate vocabulary and grammar (e.g., "Nous devons lancer la ___ bientôt." - "We need to launch the ___ soon").
205
- - **Advanced**: Feature complex structures and specialized terms tied to the domain (e.g., "L’analyse des ___ est cruciale." - "The analysis of ___ is crucial").
206
-
207
- 5. **Handle edge cases**:
208
- - For users with multiple domains (e.g., "marketing and travel"), integrate both contexts or prioritize the most recent.
209
- - If proficiency evolves (e.g., beginner to intermediate), adapt subsequent exercises accordingly.
210
-
211
- ### Output Format
212
- Produce exactly **5 cloze-style exercises** as a **valid JSON array**, with each item containing:
213
- - `"sentence"`: A sentence in the **target language** with a blank `'___'` for a missing vocabulary word or grammar element, relevant to the user’s domain.
214
- - `"answer"`: The correct word or phrase to fill in the blank.
215
- - `"choices"`: A list of 3 plausible options (including the correct answer) in the target language. Distractors should:
216
- - Be grammatically correct but unfit for the sentence’s context.
217
- - Relate to the domain but not the specific scenario (e.g., for "campagne," use "produit" but not "réunion").
218
- - Encourage critical thinking about meaning and usage.
219
-
220
- ### Example Query and Expected Output
221
-
222
- #### Example Query:
223
- User: "Beginner French exercises about my work in marketing (native: English)"
224
-
225
- #### Example Output:
226
- ```json
227
- [
228
- {"sentence": "Nous devons lancer la nouvelle ___ le mois prochain.", "answer": "campagne", "choices": ["campagne", "produit", "réunion"]},
229
- {"sentence": "Quel est le ___ principal de ce projet ?", "answer": "objectif", "choices": ["client", "objectif", "budget"]},
230
- {"sentence": "Il faut analyser le ___ avant de prendre une décision.", "answer": "marché", "choices": ["marché", "bureau", "téléphone"]},
231
- {"sentence": "Elle prépare une ___ pour les clients.", "answer": "présentation", "choices": ["facture", "présentation", "publicité"]},
232
- {"sentence": "Nous utilisons les ___ sociaux pour la promotion.", "answer": "réseaux", "choices": ["médias", "réseaux", "journaux"]}
233
- ]
234
- """
235
-
236
- simulation_mode_instructions = """
237
- # Metadata:
238
- # Native language: {native_language}
239
- # Target language: {target_language}
240
- # Proficiency level: {proficiency}
241
-
242
- You are a **creative, context-aware storytelling engine**. Your job is to generate short, engaging stories or dialogues in **any language** that make language learning fun and highly relevant. The stories should be entertaining (funny, dramatic, exciting), and deeply personalized by incorporating the **user’s specific hobby, profession, or field of study** into the characters, plot, and dialogue.
243
-
244
- ### Context Format
245
- You will receive a list of prior messages:
246
- [
247
- {"role": "user", "content": "<user input>"},
248
- {"role": "assistant", "content": "<last generated story>"}
249
- ]
250
- Treat this list as prior conversation history. Use it to:
251
- - Avoid repeating ideas, themes, or jokes from previous responses.
252
- - Build on past tone, vocabulary, or characters if appropriate.
253
- - Adjust story complexity based on past user proficiency or feedback cues.
254
-
255
- ### Story Generation Task
256
- From the latest user message:
257
- 1. **Use the provided metadata**:
258
- - **Native language**: The user’s base language for understanding.
259
- - **Target language**: The language the user is learning.
260
- - **Proficiency level**: Adjust the complexity of the story or dialogue based on the user’s proficiency level.
261
-
262
- 2. **Domain relevance**:
263
- - Focus on the **user's domain of interest** (e.g., work, hobby, field of study).
264
- - Use **realistic terminology or scenarios** related to their interests to make the story engaging and practical.
265
-
266
- 3. **Adjust story complexity**:
267
- - For **beginner** learners, keep sentences simple and direct with basic vocabulary and grammar.
268
- - For **intermediate** learners, use natural dialogue, simple narrative structures, and introduce moderately challenging vocabulary.
269
- - For **advanced** learners, incorporate idiomatic expressions, complex sentence structures, and domain-specific language.
270
-
271
- 4. **Avoid repetition**:
272
- - Ensure that new stories or dialogues bring fresh content and characters. Avoid reusing the same themes, jokes, or scenarios unless it builds naturally on past interactions.
273
-
274
- 5. **Engage with the user’s tone and interests**:
275
- - If the user is passionate about a specific topic (e.g., cooking, space exploration, or law), integrate that into the story. If the user likes humor, use a fun tone; for drama or excitement, make the story engaging with conflict or high stakes.
276
-
277
- ### Output Format
278
- Return a valid **JSON object** with the following structure:
279
- - `"title"`: An engaging title in the **native language**.
280
- - `"setting"`: A short setup in the **native language** explaining the story’s background, tailored to the user’s interest.
281
- - `"content"`: A list of **6–10 segments**, each containing:
282
- - `"speaker"`: Name or role of the speaker in the **native language** (e.g., "Narrator", "Professor Lee", "The Engineer").
283
- - `"target_language_text"`: Sentence in the **target language**.
284
- - `"phonetics"`: Standardized phonetic transcription (IPA, Pinyin, etc.) if applicable and helpful. Omit if unavailable or not useful.
285
- - `"base_language_translation"`: Simple translation of the sentence in the **native language**.
286
-
287
- ### Personalization Rules
288
- - Base the humor, conflict, and events directly on the user’s interest. For example:
289
- - If the user loves space, create an exciting stargazing story.
290
- - If they study law, create a courtroom dialogue with legal terms.
291
- - If they’re into cooking, make the story about a cooking adventure.
292
- - Include real terminology or realistic situations from the domain to make learning useful and immersive.
293
- - Adjust the tone and vocabulary complexity based on user proficiency level (beginner = simple, intermediate = natural, advanced = idiomatic).
294
- - Keep the pacing tight — avoid overly long narrations or explanations.
295
-
296
- ### Output Instructions
297
- Return only the final **JSON object**. Do not include:
298
- - Explanations
299
- - Notes
300
- - Comments
301
- - Markdown formatting
302
-
303
- ### Example User Input
304
- "Funny story for intermediate French learner about cooking hobby (base: English)"
305
-
306
- ### Example Output (French)
307
- ```json
308
- {
309
- "title": "La Panique de la Paella",
310
- "setting": "Pierre essaie d'impressionner ses amis en cuisinant une paella espagnole authentique pour la première fois.",
311
- "content": [
312
- {
313
- "speaker": "Narrateur",
314
- "target_language_text": "Pierre regarda la recette de paella. Cela semblait facile.",
315
- "phonetics": "pjeʁ ʁəɡaʁda la ʁesɛt də paɛʎa. sə.la sɛ̃blɛ ɛ.fa.sil",
316
- "base_language_translation": "Pierre looked at the paella recipe. It seemed easy."
317
- },
318
- {
319
- "speaker": "Pierre",
320
- "target_language_text": "Il me faut du safran! Où est le safran?",
321
- "phonetics": "il mə fo dy sa.fʁɑ̃! u ɛ lə sa.fʁɑ̃",
322
- "base_language_translation": "I need saffron! Where is the saffron?"
323
- },
324
- {
325
- "speaker": "Narrateur",
326
- "target_language_text": "Pierre fouilla le placard, mais il ne trouva pas de safran.",
327
- "phonetics": "pjeʁ fwi.jɑ lə pla.kɑʁ, mɛ il nə tʁu.va pa də sa.fʁɑ̃",
328
- "base_language_translation": "Pierre searched the cupboard, but he couldn’t find any saffron."
329
- },
330
- {
331
- "speaker": "Pierre",
332
- "target_language_text": "Qu'est-ce que je vais faire maintenant ?",
333
- "phonetics": "kɛs.kə ʒə vɛ fɛʁ mɛ̃tə.nɑ̃?",
334
- "base_language_translation": "What am I going to do now?"
335
- },
336
- {
337
- "speaker": "Narrateur",
338
- "target_language_text": "Finalement, Pierre décida de remplacer le safran par du curcuma.",
339
- "phonetics": "fi.nal.mɑ̃ pjeʁ de.si.da də ʁɑ̃.pla.sə lə sa.fʁɑ̃ paʁ dy kyʁ.ky.ma",
340
- "base_language_translation": "Finally, Pierre decided to replace the saffron with turmeric."
341
- },
342
- {
343
- "speaker": "Pierre",
344
- "target_language_text": "C'est presque pareil, non ?",
345
- "phonetics": "sɛ pʁɛs.kə paʁɛj, nɔ̃?",
346
- "base_language_translation": "It's almost the same, right?"
347
- }
348
- ]
349
- }
350
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v1/prev_backend_v/database.py DELETED
@@ -1,293 +0,0 @@
1
- import psycopg2
2
- import os
3
- from psycopg2 import sql
4
- from dotenv import load_dotenv
5
-
6
- load_dotenv()
7
-
8
- # Database Configuration from environment variables
9
- DB_NAME = os.getenv("POSTGRES_DB", "linguaai")
10
- DB_USER = os.getenv("POSTGRES_USER", "linguaai_user")
11
- DB_PASSWORD = os.getenv("POSTGRES_PASSWORD", "LinguaAI1008")
12
- DB_HOST = os.getenv("DB_HOST", "localhost")
13
- DB_PORT = os.getenv("DB_PORT", "5432")
14
-
15
- # SQL Schema Definition
16
- SCHEMA_SQL = """
17
- -- Drop existing objects if they exist
18
- -- Note: Some drops below might be for tables not defined in this specific script.
19
- DROP TABLE IF EXISTS user_activity_progress CASCADE;
20
- DROP TABLE IF EXISTS activities CASCADE;
21
- DROP TABLE IF EXISTS weekly_modules CASCADE;
22
- DROP TABLE IF EXISTS curriculums CASCADE;
23
- DROP TABLE IF EXISTS generated_flashcards CASCADE;
24
- DROP TABLE IF EXISTS flashcard_sets CASCADE; -- Corrected name
25
- DROP TABLE IF EXISTS generated_exercises CASCADE;
26
- DROP TABLE IF EXISTS exercise_sets CASCADE; -- Corrected name
27
- DROP TABLE IF EXISTS simulations CASCADE; -- Corrected name
28
- DROP TABLE IF EXISTS users CASCADE;
29
- DROP TYPE IF EXISTS activity_status CASCADE;
30
-
31
- -- Table `users`
32
- CREATE TABLE users (
33
- user_id SERIAL PRIMARY KEY,
34
- username VARCHAR(50) UNIQUE NOT NULL,
35
- email VARCHAR(100) UNIQUE NOT NULL,
36
- password_hash VARCHAR(255) NOT NULL,
37
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
38
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
39
- );
40
-
41
- -- Trigger function (remains the same)
42
- CREATE OR REPLACE FUNCTION update_updated_at_column()
43
- RETURNS TRIGGER AS $$
44
- BEGIN
45
- NEW.updated_at = now();
46
- RETURN NEW;
47
- END;
48
- $$ language 'plpgsql';
49
-
50
- -- Trigger for users (remains the same)
51
- CREATE TRIGGER users_update_updated_at
52
- BEFORE UPDATE ON users
53
- FOR EACH ROW
54
- EXECUTE FUNCTION update_updated_at_column();
55
-
56
-
57
- -- ============================================
58
- -- Tables for Generated Content (Flashcards)
59
- -- ============================================
60
-
61
- -- Table `flashcard_sets` (Represents one request/query)
62
- CREATE TABLE flashcard_sets (
63
- id SERIAL PRIMARY KEY,
64
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
65
- query TEXT NOT NULL,
66
- flashcards JSONB NOT NULL, -- Stores an array of 5 flashcards
67
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
69
- );
70
-
71
- CREATE INDEX idx_flashcard_set_user ON flashcard_sets(user_id);
72
-
73
- -- Corrected Trigger definition for flashcard_sets
74
- CREATE TRIGGER flashcard_sets_update_updated_at -- Renamed trigger
75
- BEFORE UPDATE ON flashcard_sets -- Corrected table name
76
- FOR EACH ROW
77
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
78
-
79
- -- Table `generated_flashcards` (Individual flashcards within a set)
80
- CREATE TABLE generated_flashcards (
81
- flashcard_id SERIAL PRIMARY KEY,
82
- set_id INT NOT NULL REFERENCES flashcard_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
83
- word TEXT NOT NULL,
84
- definition TEXT NOT NULL,
85
- example TEXT, -- Example might be optional
86
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
87
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
88
- );
89
-
90
- CREATE INDEX idx_flashcard_set ON generated_flashcards(set_id);
91
-
92
- -- Trigger for generated_flashcards (remains the same)
93
- CREATE TRIGGER generated_flashcards_update_updated_at
94
- BEFORE UPDATE ON generated_flashcards
95
- FOR EACH ROW
96
- EXECUTE FUNCTION update_updated_at_column();
97
-
98
-
99
- -- ============================================
100
- -- Tables for Generated Content (Exercises)
101
- -- ============================================
102
-
103
- -- Table `exercise_sets` (Represents one request/query) -- Corrected comment
104
- CREATE TABLE exercise_sets (
105
- id SERIAL PRIMARY KEY,
106
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
107
- query TEXT NOT NULL,
108
- exercises JSONB NOT NULL, -- Array of 5 exercises
109
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
111
- );
112
-
113
- CREATE INDEX idx_exercise_set_user ON exercise_sets(user_id); -- Corrected table name (was already correct but double-checked)
114
-
115
- -- Corrected Trigger definition for exercise_sets
116
- CREATE TRIGGER exercise_sets_update_updated_at -- Renamed trigger
117
- BEFORE UPDATE ON exercise_sets -- Corrected table name
118
- FOR EACH ROW
119
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
120
-
121
- -- Table `generated_exercises` (Individual exercises within a set)
122
- CREATE TABLE generated_exercises (
123
- exercise_id SERIAL PRIMARY KEY,
124
- set_id INT NOT NULL REFERENCES exercise_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
125
- sentence TEXT NOT NULL,
126
- answer TEXT NOT NULL,
127
- choices JSONB NOT NULL, -- Storing the array of choices
128
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
129
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
130
- );
131
-
132
- CREATE INDEX idx_exercise_set ON generated_exercises(set_id);
133
-
134
- -- Trigger for generated_exercises (remains the same)
135
- CREATE TRIGGER generated_exercises_update_updated_at
136
- BEFORE UPDATE ON generated_exercises
137
- FOR EACH ROW
138
- EXECUTE FUNCTION update_updated_at_column();
139
-
140
-
141
- -- ============================================
142
- -- Table for Generated Content (Simulations)
143
- -- ============================================
144
-
145
- -- Table `simulations` (Represents one simulation request/result) -- Corrected comment
146
- CREATE TABLE simulations (
147
- id SERIAL PRIMARY KEY,
148
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
149
- query TEXT NOT NULL,
150
- scenario TEXT NOT NULL,
151
- dialog JSONB NOT NULL, -- Array of turns with 'role', 'chinese', 'pinyin', 'english'
152
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
154
- );
155
-
156
- CREATE INDEX idx_simulation_user ON simulations(user_id); -- Corrected table name
157
-
158
- -- Corrected Trigger definition for simulations
159
- CREATE TRIGGER simulations_update_updated_at -- Renamed trigger
160
- BEFORE UPDATE ON simulations -- Corrected table name
161
- FOR EACH ROW
162
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
163
- """
164
-
165
- def get_db_connection():
166
- """Get a synchronous database connection."""
167
- try:
168
- conn = psycopg2.connect(
169
- dbname=DB_NAME,
170
- user=DB_USER,
171
- password=DB_PASSWORD,
172
- host=DB_HOST,
173
- port=DB_PORT
174
- )
175
- return conn
176
- except psycopg2.Error as e:
177
- print(f"Database connection error: {e}")
178
- raise
179
-
180
- def reset_sequences():
181
- """Generate SQL to reset all sequences (auto-incrementing IDs) to 1."""
182
- sequences_sql = """
183
- SELECT 'ALTER SEQUENCE ' || sequence_name || ' RESTART WITH 1;'
184
- FROM information_schema.sequences
185
- WHERE sequence_schema = 'public';
186
- """
187
- return sequences_sql
188
-
189
- def reset_database(confirm=True):
190
- """Reset the database by dropping all tables and recreating them."""
191
- if confirm:
192
- user_confirm = input("WARNING: This will DELETE ALL DATA. Type 'yes' to proceed: ")
193
- if user_confirm.lower() != 'yes':
194
- print("Database reset cancelled.")
195
- return
196
-
197
- conn = None
198
- try:
199
- conn = get_db_connection()
200
- conn.autocommit = False
201
- print("Database connection established.")
202
-
203
- with conn.cursor() as cur:
204
- print("Dropping and recreating schema...")
205
- # Execute the main schema SQL (includes drops)
206
- cur.execute(SCHEMA_SQL)
207
- print("Schema recreated successfully.")
208
-
209
- # Generate and execute sequence reset SQL
210
- print("Resetting sequences...")
211
- reset_sql_query = reset_sequences()
212
- cur.execute(reset_sql_query)
213
- reset_commands = cur.fetchall()
214
- for command in reset_commands:
215
- cur.execute(command[0])
216
- print("Sequences reset successfully.")
217
-
218
- conn.commit()
219
- print("Database reset complete.")
220
-
221
- except psycopg2.Error as e:
222
- print(f"Database error during reset: {e}")
223
- if conn:
224
- conn.rollback()
225
- print("Transaction rolled back.")
226
- except Exception as e:
227
- print(f"An unexpected error occurred during reset: {e}")
228
- if conn:
229
- conn.rollback()
230
- finally:
231
- if conn:
232
- conn.close()
233
- print("Database connection closed.")
234
-
235
- def setup_database(confirm=True):
236
- """Set up the database schema if tables do not exist."""
237
- if confirm:
238
- user_confirm = input("Do you want to set up the database? Type 'yes' to proceed: ")
239
- if user_confirm.lower() != 'yes':
240
- print("Database setup cancelled.")
241
- return
242
-
243
- conn = None
244
- try:
245
- conn = get_db_connection()
246
- conn.autocommit = False
247
- print("Database connection established.")
248
-
249
- with conn.cursor() as cur:
250
- print("Checking if tables exist...")
251
- cur.execute("""
252
- SELECT EXISTS (
253
- SELECT FROM information_schema.tables
254
- WHERE table_schema = 'public'
255
- AND table_name = 'users'
256
- );
257
- """)
258
- tables_exist = cur.fetchone()[0]
259
-
260
- if tables_exist:
261
- print("Tables already exist. Use reset_database() to reset the database or run setup with confirm=False.")
262
- conn.rollback() # Rollback as no changes should be made
263
- return
264
-
265
- print("Creating schema...")
266
- cur.execute(SCHEMA_SQL)
267
- print("Schema created successfully.")
268
-
269
- conn.commit()
270
- print("Database setup complete.")
271
-
272
- except psycopg2.Error as e:
273
- print(f"Database error during setup: {e}")
274
- if conn:
275
- conn.rollback()
276
- print("Transaction rolled back.")
277
- except Exception as e:
278
- print(f"An unexpected error occurred during setup: {e}")
279
- if conn:
280
- conn.rollback()
281
- finally:
282
- if conn:
283
- conn.close()
284
- print("Database connection closed.")
285
-
286
- if __name__ == "__main__":
287
- action = input("Enter 'setup' to setup database or 'reset' to reset database: ").lower()
288
- if action == 'reset':
289
- reset_database()
290
- elif action == 'setup':
291
- setup_database()
292
- else:
293
- print("Invalid action. Use 'setup' or 'reset'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v1/prev_backend_v/main.py DELETED
@@ -1,177 +0,0 @@
1
- from fastapi import FastAPI, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from backend.utils import generate_completions
6
- from backend import config
7
- from backend.database import get_db_connection
8
- import psycopg2
9
- from psycopg2.extras import RealDictCursor
10
- from typing import Union, List, Literal, Optional
11
- import logging
12
- import json
13
-
14
- logging.basicConfig(level=logging.INFO)
15
-
16
- app = FastAPI()
17
-
18
- # Add CORS middleware
19
- app.add_middleware(
20
- CORSMiddleware,
21
- allow_origins=["*"], # Allows all origins
22
- allow_credentials=True,
23
- allow_methods=["*"], # Allows all methods
24
- allow_headers=["*"], # Allows all headers
25
- )
26
-
27
- # Dependency to get database connection
28
- async def get_db():
29
- conn = await get_db_connection()
30
- try:
31
- yield conn
32
- finally:
33
- conn.close()
34
-
35
- class Message(BaseModel):
36
- role: Literal["user", "assistant"]
37
- content: str
38
-
39
- class GenerationRequest(BaseModel):
40
- user_id: int
41
- query: Union[str, List[Message]]
42
-
43
- class MetadataRequest(BaseModel):
44
- query: str
45
-
46
- # Global metadata variables
47
- native_language: Optional[str] = None
48
- target_language: Optional[str] = None
49
- proficiency: Optional[str] = None
50
-
51
- @app.get("/")
52
- async def root():
53
- return {"message": "Welcome to the AI Learning Assistant API!"}
54
-
55
- @app.post("/extract/metadata")
56
- async def extract_metadata(data: MetadataRequest):
57
- logging.info(f"Query: {data.query}")
58
- try:
59
- response_str = await generate_completions.get_completions(
60
- data.query,
61
- config.language_metadata_extraction_prompt
62
- )
63
- metadata_dict = json.loads(response_str)
64
- # Update globals for other endpoints
65
- globals()['native_language'] = metadata_dict.get('native_language', 'unknown')
66
- globals()['target_language'] = metadata_dict.get('target_language', 'unknown')
67
- globals()['proficiency'] = metadata_dict.get('proficiency_level', 'unknown')
68
- return JSONResponse(
69
- content={
70
- "data": metadata_dict,
71
- "type": "language_metadata",
72
- "status": "success"
73
- },
74
- status_code=200
75
- )
76
- except Exception as e:
77
- raise HTTPException(status_code=500, detail=str(e))
78
-
79
- @app.post("/generate/curriculum")
80
- async def generate_curriculum(data: GenerationRequest):
81
- try:
82
- # Use previously extracted metadata
83
- instructions = (
84
- config.curriculum_instructions
85
- .replace("{native_language}", native_language or "unknown")
86
- .replace("{target_language}", target_language or "unknown")
87
- .replace("{proficiency}", proficiency or "unknown")
88
- )
89
- response = await generate_completions.get_completions(
90
- data.query,
91
- instructions
92
- )
93
- return JSONResponse(
94
- content={
95
- "data": response,
96
- "type": "curriculum",
97
- "status": "success"
98
- },
99
- status_code=200
100
- )
101
- except Exception as e:
102
- raise HTTPException(status_code=500, detail=str(e))
103
-
104
- @app.post("/generate/flashcards")
105
- async def generate_flashcards(data: GenerationRequest):
106
- try:
107
- # Use previously extracted metadata
108
- instructions = (
109
- config.flashcard_mode_instructions
110
- .replace("{native_language}", native_language or "unknown")
111
- .replace("{target_language}", target_language or "unknown")
112
- .replace("{proficiency}", proficiency or "unknown")
113
- )
114
- response = await generate_completions.get_completions(
115
- data.query,
116
- instructions
117
- )
118
- return JSONResponse(
119
- content={
120
- "data": response,
121
- "type": "flashcards",
122
- "status": "success"
123
- },
124
- status_code=200
125
- )
126
- except Exception as e:
127
- raise HTTPException(status_code=500, detail=str(e))
128
-
129
- @app.post("/generate/exercises")
130
- async def generate_exercises(data: GenerationRequest):
131
- try:
132
- # Use previously extracted metadata
133
- instructions = (
134
- config.exercise_mode_instructions
135
- .replace("{native_language}", native_language or "unknown")
136
- .replace("{target_language}", target_language or "unknown")
137
- .replace("{proficiency}", proficiency or "unknown")
138
- )
139
- response = await generate_completions.get_completions(
140
- data.query,
141
- instructions
142
- )
143
- return JSONResponse(
144
- content={
145
- "data": response,
146
- "type": "exercises",
147
- "status": "success"
148
- },
149
- status_code=200
150
- )
151
- except Exception as e:
152
- raise HTTPException(status_code=500, detail=str(e))
153
-
154
- @app.post("/generate/simulation")
155
- async def generate_simulation(data: GenerationRequest):
156
- try:
157
- # Use previously extracted metadata
158
- instructions = (
159
- config.simulation_mode_instructions
160
- .replace("{native_language}", native_language or "unknown")
161
- .replace("{target_language}", target_language or "unknown")
162
- .replace("{proficiency}", proficiency or "unknown")
163
- )
164
- response = await generate_completions.get_completions(
165
- data.query,
166
- instructions
167
- )
168
- return JSONResponse(
169
- content={
170
- "data": response,
171
- "type": "simulation",
172
- "status": "success"
173
- },
174
- status_code=200
175
- )
176
- except Exception as e:
177
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v1/prev_backend_v/utils/__pycache__/generate_completions.cpython-310.pyc DELETED
Binary file (2.55 kB)
 
prev_backend_v1/prev_backend_v/utils/__pycache__/generate_completions.cpython-312.pyc DELETED
Binary file (3.7 kB)
 
prev_backend_v1/prev_backend_v/utils/generate_completions.py DELETED
@@ -1,107 +0,0 @@
1
- from openai import AsyncOpenAI, OpenAI
2
- import asyncio
3
- import json
4
- from typing import AsyncIterator
5
- from typing import Union, List, Dict, Literal
6
- from dotenv import load_dotenv
7
- import os
8
- from pydantic import BaseModel
9
- load_dotenv()
10
-
11
- # Initialize the async client
12
- client = AsyncOpenAI(
13
- base_url=os.getenv("BASE_URL"),
14
- api_key=os.getenv("API_KEY"),
15
- )
16
-
17
- class Message(BaseModel):
18
- role: Literal["user", "assistant"]
19
- content: str
20
-
21
- # Helper function to flatten chat messages into a single string prompt
22
- def flatten_messages(messages: List[Message]) -> str:
23
- return "\n".join([f"{m.role}: {m.content}" for m in messages])
24
-
25
- def process_input(data: Union[str, List[Dict[str, str]]]) -> Union[str, List[Dict[str, str]]]:
26
- """
27
- Processes input to either uppercase a string or modify the 'content' field
28
- of a list of dictionaries.
29
- """
30
- if isinstance(data, str):
31
- return data.strip() # Ensures prompt is cleaned up (optional)
32
-
33
- elif isinstance(data, list):
34
- # Ensure each item in the list is a dictionary with a 'content' key
35
- return [
36
- {**item, "content": item["content"].strip()} # Trims whitespace in 'content'
37
- for item in data if isinstance(item, dict) and "content" in item
38
- ]
39
-
40
- else:
41
- raise TypeError("Input must be a string or a list of dictionaries with a 'content' field")
42
-
43
-
44
- # async def get_completions(
45
- # prompt: Union[str, List[Dict[str, str]]],
46
- # instructions: str
47
- # ) -> str:
48
- # processed_prompt = process_input(prompt) # Ensures the input format is correct
49
-
50
- # if isinstance(processed_prompt, str):
51
- # messages = [
52
- # {"role": "system", "content": instructions},
53
- # {"role": "user", "content": processed_prompt}
54
- # ]
55
- # elif isinstance(processed_prompt, list):
56
- # messages = [{"role": "system", "content": instructions}] + processed_prompt
57
- # else:
58
- # raise TypeError("Unexpected processed input type.")
59
-
60
- # response = await client.chat.completions.create(
61
- # model=os.getenv("MODEL"),
62
- # messages=messages,
63
- # response_format={"type": "json_object"}
64
- # )
65
-
66
- # output: str = response.choices[0].message.content
67
- # return output
68
-
69
- async def get_completions(
70
- prompt: Union[str, List[Dict[str, str]]],
71
- instructions: str
72
- ) -> str:
73
- if isinstance(prompt, list):
74
- formatted_query = flatten_messages(prompt)
75
- else:
76
- formatted_query = prompt
77
-
78
- processed_prompt = process_input(formatted_query)
79
-
80
- messages = [{"role": "system", "content": instructions}]
81
-
82
- if isinstance(processed_prompt, str):
83
- messages.append({"role": "user", "content": processed_prompt})
84
-
85
- elif isinstance(processed_prompt, list):
86
- # Only keep the history for context and append the latest user query at the end
87
- history = processed_prompt[:-1]
88
- last_user_msg = processed_prompt[-1]
89
-
90
- # Optional: Validate that the last message is from the user
91
- if last_user_msg.get("role") != "user":
92
- raise ValueError("Last message must be from the user.")
93
-
94
- messages += history
95
- messages.append(last_user_msg)
96
-
97
- else:
98
- raise TypeError("Unexpected processed input type.")
99
-
100
- # print(os.getenv("MODEL"))
101
- response = await client.chat.completions.create(
102
- model=os.getenv("MODEL"),
103
- messages=messages,
104
- response_format={"type": "json_object"}
105
- )
106
-
107
- return response.choices[0].message.content # adjust based on your client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v2/backend/__pycache__/config.cpython-310.pyc DELETED
Binary file (12 kB)
 
prev_backend_v2/backend/__pycache__/config.cpython-312.pyc DELETED
Binary file (17.4 kB)
 
prev_backend_v2/backend/__pycache__/database.cpython-310.pyc DELETED
Binary file (10.1 kB)
 
prev_backend_v2/backend/__pycache__/database.cpython-312.pyc DELETED
Binary file (12.6 kB)
 
prev_backend_v2/backend/__pycache__/main.cpython-310.pyc DELETED
Binary file (3.28 kB)
 
prev_backend_v2/backend/__pycache__/main.cpython-312.pyc DELETED
Binary file (8.49 kB)
 
prev_backend_v2/backend/database.py DELETED
@@ -1,293 +0,0 @@
1
- import psycopg2
2
- import os
3
- from psycopg2 import sql
4
- from dotenv import load_dotenv
5
-
6
- load_dotenv()
7
-
8
- # Database Configuration from environment variables
9
- DB_NAME = os.getenv("POSTGRES_DB", "linguaai")
10
- DB_USER = os.getenv("POSTGRES_USER", "linguaai_user")
11
- DB_PASSWORD = os.getenv("POSTGRES_PASSWORD", "LinguaAI1008")
12
- DB_HOST = os.getenv("DB_HOST", "localhost")
13
- DB_PORT = os.getenv("DB_PORT", "5432")
14
-
15
- # SQL Schema Definition
16
- SCHEMA_SQL = """
17
- -- Drop existing objects if they exist
18
- -- Note: Some drops below might be for tables not defined in this specific script.
19
- DROP TABLE IF EXISTS user_activity_progress CASCADE;
20
- DROP TABLE IF EXISTS activities CASCADE;
21
- DROP TABLE IF EXISTS weekly_modules CASCADE;
22
- DROP TABLE IF EXISTS curriculums CASCADE;
23
- DROP TABLE IF EXISTS generated_flashcards CASCADE;
24
- DROP TABLE IF EXISTS flashcard_sets CASCADE; -- Corrected name
25
- DROP TABLE IF EXISTS generated_exercises CASCADE;
26
- DROP TABLE IF EXISTS exercise_sets CASCADE; -- Corrected name
27
- DROP TABLE IF EXISTS simulations CASCADE; -- Corrected name
28
- DROP TABLE IF EXISTS users CASCADE;
29
- DROP TYPE IF EXISTS activity_status CASCADE;
30
-
31
- -- Table `users`
32
- CREATE TABLE users (
33
- user_id SERIAL PRIMARY KEY,
34
- username VARCHAR(50) UNIQUE NOT NULL,
35
- email VARCHAR(100) UNIQUE NOT NULL,
36
- password_hash VARCHAR(255) NOT NULL,
37
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
38
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
39
- );
40
-
41
- -- Trigger function (remains the same)
42
- CREATE OR REPLACE FUNCTION update_updated_at_column()
43
- RETURNS TRIGGER AS $$
44
- BEGIN
45
- NEW.updated_at = now();
46
- RETURN NEW;
47
- END;
48
- $$ language 'plpgsql';
49
-
50
- -- Trigger for users (remains the same)
51
- CREATE TRIGGER users_update_updated_at
52
- BEFORE UPDATE ON users
53
- FOR EACH ROW
54
- EXECUTE FUNCTION update_updated_at_column();
55
-
56
-
57
- -- ============================================
58
- -- Tables for Generated Content (Flashcards)
59
- -- ============================================
60
-
61
- -- Table `flashcard_sets` (Represents one request/query)
62
- CREATE TABLE flashcard_sets (
63
- id SERIAL PRIMARY KEY,
64
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
65
- query TEXT NOT NULL,
66
- flashcards JSONB NOT NULL, -- Stores an array of 5 flashcards
67
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
69
- );
70
-
71
- CREATE INDEX idx_flashcard_set_user ON flashcard_sets(user_id);
72
-
73
- -- Corrected Trigger definition for flashcard_sets
74
- CREATE TRIGGER flashcard_sets_update_updated_at -- Renamed trigger
75
- BEFORE UPDATE ON flashcard_sets -- Corrected table name
76
- FOR EACH ROW
77
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
78
-
79
- -- Table `generated_flashcards` (Individual flashcards within a set)
80
- CREATE TABLE generated_flashcards (
81
- flashcard_id SERIAL PRIMARY KEY,
82
- set_id INT NOT NULL REFERENCES flashcard_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
83
- word TEXT NOT NULL,
84
- definition TEXT NOT NULL,
85
- example TEXT, -- Example might be optional
86
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
87
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
88
- );
89
-
90
- CREATE INDEX idx_flashcard_set ON generated_flashcards(set_id);
91
-
92
- -- Trigger for generated_flashcards (remains the same)
93
- CREATE TRIGGER generated_flashcards_update_updated_at
94
- BEFORE UPDATE ON generated_flashcards
95
- FOR EACH ROW
96
- EXECUTE FUNCTION update_updated_at_column();
97
-
98
-
99
- -- ============================================
100
- -- Tables for Generated Content (Exercises)
101
- -- ============================================
102
-
103
- -- Table `exercise_sets` (Represents one request/query) -- Corrected comment
104
- CREATE TABLE exercise_sets (
105
- id SERIAL PRIMARY KEY,
106
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
107
- query TEXT NOT NULL,
108
- exercises JSONB NOT NULL, -- Array of 5 exercises
109
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
111
- );
112
-
113
- CREATE INDEX idx_exercise_set_user ON exercise_sets(user_id); -- Corrected table name (was already correct but double-checked)
114
-
115
- -- Corrected Trigger definition for exercise_sets
116
- CREATE TRIGGER exercise_sets_update_updated_at -- Renamed trigger
117
- BEFORE UPDATE ON exercise_sets -- Corrected table name
118
- FOR EACH ROW
119
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
120
-
121
- -- Table `generated_exercises` (Individual exercises within a set)
122
- CREATE TABLE generated_exercises (
123
- exercise_id SERIAL PRIMARY KEY,
124
- set_id INT NOT NULL REFERENCES exercise_sets(id) ON DELETE CASCADE, -- Corrected FK reference (table and column)
125
- sentence TEXT NOT NULL,
126
- answer TEXT NOT NULL,
127
- choices JSONB NOT NULL, -- Storing the array of choices
128
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
129
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
130
- );
131
-
132
- CREATE INDEX idx_exercise_set ON generated_exercises(set_id);
133
-
134
- -- Trigger for generated_exercises (remains the same)
135
- CREATE TRIGGER generated_exercises_update_updated_at
136
- BEFORE UPDATE ON generated_exercises
137
- FOR EACH ROW
138
- EXECUTE FUNCTION update_updated_at_column();
139
-
140
-
141
- -- ============================================
142
- -- Table for Generated Content (Simulations)
143
- -- ============================================
144
-
145
- -- Table `simulations` (Represents one simulation request/result) -- Corrected comment
146
- CREATE TABLE simulations (
147
- id SERIAL PRIMARY KEY,
148
- user_id INTEGER NOT NULL REFERENCES users(user_id), -- Added FK reference for completeness
149
- query TEXT NOT NULL,
150
- scenario TEXT NOT NULL,
151
- dialog JSONB NOT NULL, -- Array of turns with 'role', 'chinese', 'pinyin', 'english'
152
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Added updated_at for consistency
154
- );
155
-
156
- CREATE INDEX idx_simulation_user ON simulations(user_id); -- Corrected table name
157
-
158
- -- Corrected Trigger definition for simulations
159
- CREATE TRIGGER simulations_update_updated_at -- Renamed trigger
160
- BEFORE UPDATE ON simulations -- Corrected table name
161
- FOR EACH ROW
162
- EXECUTE FUNCTION update_updated_at_column(); -- Assumes you want updated_at here too
163
- """
164
-
165
- def get_db_connection():
166
- """Get a synchronous database connection."""
167
- try:
168
- conn = psycopg2.connect(
169
- dbname=DB_NAME,
170
- user=DB_USER,
171
- password=DB_PASSWORD,
172
- host=DB_HOST,
173
- port=DB_PORT
174
- )
175
- return conn
176
- except psycopg2.Error as e:
177
- print(f"Database connection error: {e}")
178
- raise
179
-
180
- def reset_sequences():
181
- """Generate SQL to reset all sequences (auto-incrementing IDs) to 1."""
182
- sequences_sql = """
183
- SELECT 'ALTER SEQUENCE ' || sequence_name || ' RESTART WITH 1;'
184
- FROM information_schema.sequences
185
- WHERE sequence_schema = 'public';
186
- """
187
- return sequences_sql
188
-
189
- def reset_database(confirm=True):
190
- """Reset the database by dropping all tables and recreating them."""
191
- if confirm:
192
- user_confirm = input("WARNING: This will DELETE ALL DATA. Type 'yes' to proceed: ")
193
- if user_confirm.lower() != 'yes':
194
- print("Database reset cancelled.")
195
- return
196
-
197
- conn = None
198
- try:
199
- conn = get_db_connection()
200
- conn.autocommit = False
201
- print("Database connection established.")
202
-
203
- with conn.cursor() as cur:
204
- print("Dropping and recreating schema...")
205
- # Execute the main schema SQL (includes drops)
206
- cur.execute(SCHEMA_SQL)
207
- print("Schema recreated successfully.")
208
-
209
- # Generate and execute sequence reset SQL
210
- print("Resetting sequences...")
211
- reset_sql_query = reset_sequences()
212
- cur.execute(reset_sql_query)
213
- reset_commands = cur.fetchall()
214
- for command in reset_commands:
215
- cur.execute(command[0])
216
- print("Sequences reset successfully.")
217
-
218
- conn.commit()
219
- print("Database reset complete.")
220
-
221
- except psycopg2.Error as e:
222
- print(f"Database error during reset: {e}")
223
- if conn:
224
- conn.rollback()
225
- print("Transaction rolled back.")
226
- except Exception as e:
227
- print(f"An unexpected error occurred during reset: {e}")
228
- if conn:
229
- conn.rollback()
230
- finally:
231
- if conn:
232
- conn.close()
233
- print("Database connection closed.")
234
-
235
- def setup_database(confirm=True):
236
- """Set up the database schema if tables do not exist."""
237
- if confirm:
238
- user_confirm = input("Do you want to set up the database? Type 'yes' to proceed: ")
239
- if user_confirm.lower() != 'yes':
240
- print("Database setup cancelled.")
241
- return
242
-
243
- conn = None
244
- try:
245
- conn = get_db_connection()
246
- conn.autocommit = False
247
- print("Database connection established.")
248
-
249
- with conn.cursor() as cur:
250
- print("Checking if tables exist...")
251
- cur.execute("""
252
- SELECT EXISTS (
253
- SELECT FROM information_schema.tables
254
- WHERE table_schema = 'public'
255
- AND table_name = 'users'
256
- );
257
- """)
258
- tables_exist = cur.fetchone()[0]
259
-
260
- if tables_exist:
261
- print("Tables already exist. Use reset_database() to reset the database or run setup with confirm=False.")
262
- conn.rollback() # Rollback as no changes should be made
263
- return
264
-
265
- print("Creating schema...")
266
- cur.execute(SCHEMA_SQL)
267
- print("Schema created successfully.")
268
-
269
- conn.commit()
270
- print("Database setup complete.")
271
-
272
- except psycopg2.Error as e:
273
- print(f"Database error during setup: {e}")
274
- if conn:
275
- conn.rollback()
276
- print("Transaction rolled back.")
277
- except Exception as e:
278
- print(f"An unexpected error occurred during setup: {e}")
279
- if conn:
280
- conn.rollback()
281
- finally:
282
- if conn:
283
- conn.close()
284
- print("Database connection closed.")
285
-
286
- if __name__ == "__main__":
287
- action = input("Enter 'setup' to setup database or 'reset' to reset database: ").lower()
288
- if action == 'reset':
289
- reset_database()
290
- elif action == 'setup':
291
- setup_database()
292
- else:
293
- print("Invalid action. Use 'setup' or 'reset'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v2/backend/main.py DELETED
@@ -1,189 +0,0 @@
1
- from fastapi import FastAPI, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from backend.utils import generate_completions
6
- from backend import config
7
- from backend.database import get_db_connection
8
- import psycopg2
9
- from psycopg2.extras import RealDictCursor
10
- from typing import Union, List, Literal, Optional
11
- import logging
12
- import json
13
-
14
- logging.basicConfig(level=logging.INFO)
15
-
16
- app = FastAPI()
17
-
18
- # Add CORS middleware
19
- app.add_middleware(
20
- CORSMiddleware,
21
- allow_origins=["*"], # Allows all origins
22
- allow_credentials=True,
23
- allow_methods=["*"], # Allows all methods
24
- allow_headers=["*"], # Allows all headers
25
- )
26
-
27
- # Dependency to get database connection
28
- async def get_db():
29
- conn = await get_db_connection()
30
- try:
31
- yield conn
32
- finally:
33
- conn.close()
34
-
35
- class Message(BaseModel):
36
- role: Literal["user", "assistant"]
37
- content: str
38
-
39
- class GenerationRequest(BaseModel):
40
- user_id: int
41
- query: Union[str, List[Message]]
42
- native_language: Optional[str] = None
43
- target_language: Optional[str] = None
44
- proficiency: Optional[str] = None
45
-
46
- class MetadataRequest(BaseModel):
47
- query: str
48
-
49
- # Global metadata variables
50
- native_language: Optional[str] = None
51
- target_language: Optional[str] = None
52
- proficiency: Optional[str] = None
53
-
54
- @app.get("/")
55
- async def root():
56
- return {"message": "Welcome to the AI Learning Assistant API!"}
57
-
58
- @app.post("/extract/metadata")
59
- async def extract_metadata(data: MetadataRequest):
60
- logging.info(f"Query: {data.query}")
61
- try:
62
- response_str = await generate_completions.get_completions(
63
- data.query,
64
- config.language_metadata_extraction_prompt
65
- )
66
- metadata_dict = json.loads(response_str)
67
- # Update globals for other endpoints
68
- globals()['native_language'] = metadata_dict.get('native_language', 'unknown')
69
- globals()['target_language'] = metadata_dict.get('target_language', 'unknown')
70
- globals()['proficiency'] = metadata_dict.get('proficiency', 'unknown')
71
- return JSONResponse(
72
- content={
73
- "data": metadata_dict,
74
- "type": "language_metadata",
75
- "status": "success"
76
- },
77
- status_code=200
78
- )
79
- except Exception as e:
80
- raise HTTPException(status_code=500, detail=str(e))
81
-
82
- @app.post("/generate/curriculum")
83
- async def generate_curriculum(data: GenerationRequest):
84
- try:
85
- # Use metadata from request or fallback to globals
86
- nl = data.native_language or native_language or "unknown"
87
- tl = data.target_language or target_language or "unknown"
88
- prof = data.proficiency or proficiency or "unknown"
89
- instructions = (
90
- config.curriculum_instructions
91
- .replace("{native_language}", nl)
92
- .replace("{target_language}", tl)
93
- .replace("{proficiency}", prof)
94
- )
95
- response = await generate_completions.get_completions(
96
- data.query,
97
- instructions
98
- )
99
- return JSONResponse(
100
- content={
101
- "data": response,
102
- "type": "curriculum",
103
- "status": "success"
104
- },
105
- status_code=200
106
- )
107
- except Exception as e:
108
- raise HTTPException(status_code=500, detail=str(e))
109
-
110
- @app.post("/generate/flashcards")
111
- async def generate_flashcards(data: GenerationRequest):
112
- try:
113
- nl = data.native_language or native_language or "unknown"
114
- tl = data.target_language or target_language or "unknown"
115
- prof = data.proficiency or proficiency or "unknown"
116
- instructions = (
117
- config.flashcard_mode_instructions
118
- .replace("{native_language}", nl)
119
- .replace("{target_language}", tl)
120
- .replace("{proficiency}", prof)
121
- )
122
- response = await generate_completions.get_completions(
123
- data.query,
124
- instructions
125
- )
126
- return JSONResponse(
127
- content={
128
- "data": response,
129
- "type": "flashcards",
130
- "status": "success"
131
- },
132
- status_code=200
133
- )
134
- except Exception as e:
135
- raise HTTPException(status_code=500, detail=str(e))
136
-
137
- @app.post("/generate/exercises")
138
- async def generate_exercises(data: GenerationRequest):
139
- try:
140
- nl = data.native_language or native_language or "unknown"
141
- tl = data.target_language or target_language or "unknown"
142
- prof = data.proficiency or proficiency or "unknown"
143
- instructions = (
144
- config.exercise_mode_instructions
145
- .replace("{native_language}", nl)
146
- .replace("{target_language}", tl)
147
- .replace("{proficiency}", prof)
148
- )
149
- response = await generate_completions.get_completions(
150
- data.query,
151
- instructions
152
- )
153
- return JSONResponse(
154
- content={
155
- "data": response,
156
- "type": "exercises",
157
- "status": "success"
158
- },
159
- status_code=200
160
- )
161
- except Exception as e:
162
- raise HTTPException(status_code=500, detail=str(e))
163
-
164
- @app.post("/generate/simulation")
165
- async def generate_simulation(data: GenerationRequest):
166
- try:
167
- nl = data.native_language or native_language or "unknown"
168
- tl = data.target_language or target_language or "unknown"
169
- prof = data.proficiency or proficiency or "unknown"
170
- instructions = (
171
- config.simulation_mode_instructions
172
- .replace("{native_language}", nl)
173
- .replace("{target_language}", tl)
174
- .replace("{proficiency}", prof)
175
- )
176
- response = await generate_completions.get_completions(
177
- data.query,
178
- instructions
179
- )
180
- return JSONResponse(
181
- content={
182
- "data": response,
183
- "type": "simulation",
184
- "status": "success"
185
- },
186
- status_code=200
187
- )
188
- except Exception as e:
189
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v2/backend/utils/__pycache__/generate_completions.cpython-310.pyc DELETED
Binary file (2.55 kB)
 
prev_backend_v2/backend/utils/generate_completions.py DELETED
@@ -1,107 +0,0 @@
1
- from openai import AsyncOpenAI, OpenAI
2
- import asyncio
3
- import json
4
- from typing import AsyncIterator
5
- from typing import Union, List, Dict, Literal
6
- from dotenv import load_dotenv
7
- import os
8
- from pydantic import BaseModel
9
- load_dotenv()
10
-
11
- # Initialize the async client
12
- client = AsyncOpenAI(
13
- base_url=os.getenv("BASE_URL"),
14
- api_key=os.getenv("API_KEY"),
15
- )
16
-
17
- class Message(BaseModel):
18
- role: Literal["user", "assistant"]
19
- content: str
20
-
21
- # Helper function to flatten chat messages into a single string prompt
22
- def flatten_messages(messages: List[Message]) -> str:
23
- return "\n".join([f"{m.role}: {m.content}" for m in messages])
24
-
25
- def process_input(data: Union[str, List[Dict[str, str]]]) -> Union[str, List[Dict[str, str]]]:
26
- """
27
- Processes input to either uppercase a string or modify the 'content' field
28
- of a list of dictionaries.
29
- """
30
- if isinstance(data, str):
31
- return data.strip() # Ensures prompt is cleaned up (optional)
32
-
33
- elif isinstance(data, list):
34
- # Ensure each item in the list is a dictionary with a 'content' key
35
- return [
36
- {**item, "content": item["content"].strip()} # Trims whitespace in 'content'
37
- for item in data if isinstance(item, dict) and "content" in item
38
- ]
39
-
40
- else:
41
- raise TypeError("Input must be a string or a list of dictionaries with a 'content' field")
42
-
43
-
44
- # async def get_completions(
45
- # prompt: Union[str, List[Dict[str, str]]],
46
- # instructions: str
47
- # ) -> str:
48
- # processed_prompt = process_input(prompt) # Ensures the input format is correct
49
-
50
- # if isinstance(processed_prompt, str):
51
- # messages = [
52
- # {"role": "system", "content": instructions},
53
- # {"role": "user", "content": processed_prompt}
54
- # ]
55
- # elif isinstance(processed_prompt, list):
56
- # messages = [{"role": "system", "content": instructions}] + processed_prompt
57
- # else:
58
- # raise TypeError("Unexpected processed input type.")
59
-
60
- # response = await client.chat.completions.create(
61
- # model=os.getenv("MODEL"),
62
- # messages=messages,
63
- # response_format={"type": "json_object"}
64
- # )
65
-
66
- # output: str = response.choices[0].message.content
67
- # return output
68
-
69
- async def get_completions(
70
- prompt: Union[str, List[Dict[str, str]]],
71
- instructions: str
72
- ) -> str:
73
- if isinstance(prompt, list):
74
- formatted_query = flatten_messages(prompt)
75
- else:
76
- formatted_query = prompt
77
-
78
- processed_prompt = process_input(formatted_query)
79
-
80
- messages = [{"role": "system", "content": instructions}]
81
-
82
- if isinstance(processed_prompt, str):
83
- messages.append({"role": "user", "content": processed_prompt})
84
-
85
- elif isinstance(processed_prompt, list):
86
- # Only keep the history for context and append the latest user query at the end
87
- history = processed_prompt[:-1]
88
- last_user_msg = processed_prompt[-1]
89
-
90
- # Optional: Validate that the last message is from the user
91
- if last_user_msg.get("role") != "user":
92
- raise ValueError("Last message must be from the user.")
93
-
94
- messages += history
95
- messages.append(last_user_msg)
96
-
97
- else:
98
- raise TypeError("Unexpected processed input type.")
99
-
100
- # print(os.getenv("MODEL"))
101
- response = await client.chat.completions.create(
102
- model=os.getenv("MODEL"),
103
- messages=messages,
104
- response_format={"type": "json_object"}
105
- )
106
-
107
- return response.choices[0].message.content # adjust based on your client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prev_backend_v4/backend/__pycache__/config.cpython-311.pyc ADDED
Binary file (14.5 kB). View file
 
prev_backend_v4/backend/__pycache__/config.cpython-312.pyc ADDED
Binary file (14.5 kB). View file
 
prev_backend_v4/backend/__pycache__/content_generator.cpython-311.pyc ADDED
Binary file (12.3 kB). View file
 
prev_backend_v4/backend/__pycache__/content_generator.cpython-312.pyc ADDED
Binary file (11 kB). View file
 
prev_backend_v4/backend/__pycache__/db.cpython-311.pyc ADDED
Binary file (31 kB). View file
 
prev_backend_v4/backend/__pycache__/db.cpython-312.pyc ADDED
Binary file (25.5 kB). View file
 
prev_backend_v4/backend/__pycache__/db_cache.cpython-311.pyc ADDED
Binary file (7.31 kB). View file
 
prev_backend_v4/backend/__pycache__/db_cache.cpython-312.pyc ADDED
Binary file (6.4 kB). View file
 
prev_backend_v4/backend/__pycache__/db_init.cpython-311.pyc ADDED
Binary file (15.7 kB). View file
 
prev_backend_v4/backend/__pycache__/db_init.cpython-312.pyc ADDED
Binary file (14 kB). View file
 
prev_backend_v4/backend/__pycache__/main.cpython-311.pyc ADDED
Binary file (20.3 kB). View file
 
prev_backend_v4/backend/__pycache__/main.cpython-312.pyc ADDED
Binary file (17.6 kB). View file
 
prev_backend_v4/backend/cache.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Any, Callable, Dict, Tuple
3
+
4
+ class AsyncLRUCache:
5
+ def __init__(self, maxsize=100_000): # Optimized for 16GB RAM
6
+ self.cache: Dict[Tuple, Any] = {}
7
+ self.order = []
8
+ self.maxsize = maxsize
9
+ self.lock = asyncio.Lock()
10
+
11
+ async def get_or_set(self, key: Tuple, coro: Callable, *args, **kwargs):
12
+ async with self.lock:
13
+ if key in self.cache:
14
+ # Move key to end to show it was recently used
15
+ self.order.remove(key)
16
+ self.order.append(key)
17
+ return self.cache[key]
18
+ # Not cached, compute result
19
+ result = await coro(*args, **kwargs)
20
+ async with self.lock:
21
+ self.cache[key] = result
22
+ self.order.append(key)
23
+ if len(self.order) > self.maxsize:
24
+ oldest = self.order.pop(0)
25
+ del self.cache[oldest]
26
+ return result
27
+
28
+ # Initialize cache with optimized size for 16GB RAM
29
+ cache = AsyncLRUCache() # Uses default maxsize=100_000
{prev_backend_v2 → prev_backend_v4}/backend/config.py RENAMED
@@ -48,7 +48,7 @@ curriculum_instructions = """
48
  You are an AI-powered language learning assistant tasked with generating an extensive, personalized curriculum. Your goal is to help the user learn {target_language} by designing a 25-lesson curriculum that reflects the user's goals, interests, and proficiency level. All outputs should be written in {native_language}.
49
 
50
  ### Curriculum Goals:
51
- - Provide 25 lessons.
52
  - Ensure logical progression from basic to advanced topics (according to {proficiency}).
53
  - Align each lesson with a practical communication goal.
54
  - Tailor vocabulary and sub-topics to the user’s intended use (e.g., work, travel, hobbies, daily life).
@@ -57,15 +57,15 @@ You are an AI-powered language learning assistant tasked with generating an exte
57
 
58
  1. **Define the Lesson Series (Overall Theme):**
59
  - Choose a main theme relevant to the user's motivation for learning {target_language} (e.g., "Living in a new country", "Professional communication", "Traveling in {target_language}-speaking regions").
60
- - The theme should guide the tone, content, and scope of the entire 25-lesson sequence.
61
 
62
- 2. **Divide the Curriculum into 25 Thematic Lessons:**
63
  - Each lesson should have a clear focus (e.g., asking for help, describing your job, booking accommodation).
64
  - Sequence lessons to build from foundational topics to more complex, specialized language use.
65
  - Vary grammar, vocabulary, and communication functions across lessons to avoid repetition and ensure comprehensive coverage.
66
 
67
  3. **Describe Each Lesson Clearly and Concisely:**
68
- For each of the 25 lessons, provide:
69
  - "sub_topic": A clear and practical lesson title in {native_language}.
70
  - "keywords": A list of 1–3 high-level categories in {native_language} that describe the lesson focus (e.g., "directions", "daily routine", "formal conversation").
71
  - "description": One sentence in {native_language} that explains what the learner will achieve or be able to do after completing the lesson. Be specific and learner-oriented.
@@ -73,7 +73,7 @@ You are an AI-powered language learning assistant tasked with generating an exte
73
  ### Output Format:
74
  Return a valid JSON object with:
75
  - "lesson_topic": The overall learning theme (in {native_language}).
76
- - "sub_topics": A list of 25 items. Each item must include:
77
  - "sub_topic": A short title of the lesson (in {native_language}).
78
  - "keywords": A list of 1–3 general-purpose categories (in {native_language}).
79
  - "description": One clear sentence (in {native_language}) describing the purpose of the lesson.
@@ -142,15 +142,12 @@ flashcard_mode_instructions = """
142
  # Native language: {native_language}
143
  # Target language: {target_language}
144
  # Proficiency level: {proficiency}
145
-
146
  You are a highly adaptive vocabulary tutor capable of teaching any language. Your goal is to help users learn rapidly by generating personalized flashcards from lesson-based content.
147
-
148
  ### Input Format
149
  You will receive a structured lesson as input (text, dialogue, or vocabulary list). Use this input to:
150
  - Identify new or useful vocabulary terms.
151
  - Extract contextually relevant and domain-specific language.
152
  - Ensure that flashcards reflect the lesson's language, style, and purpose.
153
-
154
  ### Generation Guidelines
155
  When generating flashcards:
156
  1. **Use the provided metadata**:
@@ -160,17 +157,14 @@ When generating flashcards:
160
  - *Beginner*: High-frequency, essential words.
161
  - *Intermediate*: Broader, topic-specific terms and common collocations.
162
  - *Advanced*: Nuanced, idiomatic, or technical vocabulary.
163
-
164
  2. **Contextual relevance**:
165
  - Flashcards should reflect the themes, activities, or domain of the lesson input (e.g., cooking, business, travel).
166
  - Ensure that example sentences are directly related to the input content and sound natural in use.
167
-
168
  3. **Avoid redundancy**:
169
  - Select terms that are novel, useful, or not overly repetitive within the lesson.
170
  - Prioritize terms that learners are likely to encounter again in real-world usage.
171
-
172
  ### Flashcard Format
173
- Generate exactly **10 flashcards** as a **valid JSON array**, with each flashcard containing:
174
  - `"word"`: A key word or phrase in {target_language} drawn from the lesson.
175
  - `"definition"`: A learner-friendly explanation in {native_language}.
176
  - `"example"`: A clear, natural sentence in {target_language} demonstrating the word **in context with the lesson**.
@@ -182,7 +176,8 @@ simulation_mode_instructions = """
182
  # Target language: {target_language}
183
  # Proficiency level: {proficiency}
184
 
185
- You are a **creative, context-aware storytelling engine**. Your task is to generate short, engaging stories or dialogues in **any language** to make language learning enjoyable, memorable, and relevant. Stories must reflect the user's interests, profession, or hobbies, and align with their learning level.
 
186
 
187
  ### Input Format
188
  You will receive a user-provided **lesson topic, theme, or domain of interest** (e.g., “a courtroom drama for a law student” or “space mission dialogue for a space enthusiast”). Use this input to:
 
48
  You are an AI-powered language learning assistant tasked with generating an extensive, personalized curriculum. Your goal is to help the user learn {target_language} by designing a 25-lesson curriculum that reflects the user's goals, interests, and proficiency level. All outputs should be written in {native_language}.
49
 
50
  ### Curriculum Goals:
51
+ - Provide 5 lessons.
52
  - Ensure logical progression from basic to advanced topics (according to {proficiency}).
53
  - Align each lesson with a practical communication goal.
54
  - Tailor vocabulary and sub-topics to the user’s intended use (e.g., work, travel, hobbies, daily life).
 
57
 
58
  1. **Define the Lesson Series (Overall Theme):**
59
  - Choose a main theme relevant to the user's motivation for learning {target_language} (e.g., "Living in a new country", "Professional communication", "Traveling in {target_language}-speaking regions").
60
+ - The theme should guide the tone, content, and scope of the entire 5-lesson sequence.
61
 
62
+ 2. **Divide the Curriculum into 5 Thematic Lessons:**
63
  - Each lesson should have a clear focus (e.g., asking for help, describing your job, booking accommodation).
64
  - Sequence lessons to build from foundational topics to more complex, specialized language use.
65
  - Vary grammar, vocabulary, and communication functions across lessons to avoid repetition and ensure comprehensive coverage.
66
 
67
  3. **Describe Each Lesson Clearly and Concisely:**
68
+ For each of the 5 lessons, provide:
69
  - "sub_topic": A clear and practical lesson title in {native_language}.
70
  - "keywords": A list of 1–3 high-level categories in {native_language} that describe the lesson focus (e.g., "directions", "daily routine", "formal conversation").
71
  - "description": One sentence in {native_language} that explains what the learner will achieve or be able to do after completing the lesson. Be specific and learner-oriented.
 
73
  ### Output Format:
74
  Return a valid JSON object with:
75
  - "lesson_topic": The overall learning theme (in {native_language}).
76
+ - "sub_topics": A list of 5 items. Each item must include:
77
  - "sub_topic": A short title of the lesson (in {native_language}).
78
  - "keywords": A list of 1–3 general-purpose categories (in {native_language}).
79
  - "description": One clear sentence (in {native_language}) describing the purpose of the lesson.
 
142
  # Native language: {native_language}
143
  # Target language: {target_language}
144
  # Proficiency level: {proficiency}
 
145
  You are a highly adaptive vocabulary tutor capable of teaching any language. Your goal is to help users learn rapidly by generating personalized flashcards from lesson-based content.
 
146
  ### Input Format
147
  You will receive a structured lesson as input (text, dialogue, or vocabulary list). Use this input to:
148
  - Identify new or useful vocabulary terms.
149
  - Extract contextually relevant and domain-specific language.
150
  - Ensure that flashcards reflect the lesson's language, style, and purpose.
 
151
  ### Generation Guidelines
152
  When generating flashcards:
153
  1. **Use the provided metadata**:
 
157
  - *Beginner*: High-frequency, essential words.
158
  - *Intermediate*: Broader, topic-specific terms and common collocations.
159
  - *Advanced*: Nuanced, idiomatic, or technical vocabulary.
 
160
  2. **Contextual relevance**:
161
  - Flashcards should reflect the themes, activities, or domain of the lesson input (e.g., cooking, business, travel).
162
  - Ensure that example sentences are directly related to the input content and sound natural in use.
 
163
  3. **Avoid redundancy**:
164
  - Select terms that are novel, useful, or not overly repetitive within the lesson.
165
  - Prioritize terms that learners are likely to encounter again in real-world usage.
 
166
  ### Flashcard Format
167
+ Generate exactly **5 flashcards** as a **valid JSON array**, with each flashcard containing:
168
  - `"word"`: A key word or phrase in {target_language} drawn from the lesson.
169
  - `"definition"`: A learner-friendly explanation in {native_language}.
170
  - `"example"`: A clear, natural sentence in {target_language} demonstrating the word **in context with the lesson**.
 
176
  # Target language: {target_language}
177
  # Proficiency level: {proficiency}
178
 
179
+ You are a **creative, context-aware storytelling engine**. Your task is to generate short, engaging stories or dialogues in **any language** to make language learning enjoyable, memorable, and relevant.
180
+ Stories must reflect the user's interests, profession, or hobbies, and align with their learning level.
181
 
182
  ### Input Format
183
  You will receive a user-provided **lesson topic, theme, or domain of interest** (e.g., “a courtroom drama for a law student” or “space mission dialogue for a space enthusiast”). Use this input to:
prev_backend_v4/backend/content_generator.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import asyncio
3
+ from typing import Dict, Any, Optional, List
4
+ from backend.utils import generate_completions
5
+ from backend import config
6
+ from backend.db import db
7
+ from backend.db_cache import api_cache
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ContentGenerator:
14
+ """Service for generating and storing all learning content"""
15
+
16
+ async def generate_curriculum_from_metadata(
17
+ self,
18
+ metadata_extraction_id: str,
19
+ query: str,
20
+ metadata: Dict[str, Any],
21
+ user_id: Optional[int] = None
22
+ ) -> str:
23
+ """Generate curriculum based on extracted metadata"""
24
+ # Format curriculum instructions with metadata
25
+ instructions = (
26
+ config.curriculum_instructions
27
+ .replace("{native_language}", metadata['native_language'])
28
+ .replace("{target_language}", metadata['target_language'])
29
+ .replace("{proficiency}", metadata['proficiency'])
30
+ )
31
+
32
+ # Generate curriculum
33
+ logger.info(f"Generating curriculum for {metadata['target_language']} ({metadata['proficiency']})")
34
+ curriculum_response = await generate_completions.get_completions(query, instructions)
35
+
36
+ try:
37
+ # Parse curriculum response
38
+ curriculum = json.loads(curriculum_response)
39
+ except json.JSONDecodeError:
40
+ logger.error(f"Failed to parse curriculum response: {curriculum_response[:200]}...")
41
+ curriculum = {"lesson_topic": "Language Learning Journey", "sub_topics": []}
42
+
43
+ # Save curriculum to database
44
+ curriculum_id = await db.save_curriculum(
45
+ metadata_extraction_id=metadata_extraction_id,
46
+ curriculum=curriculum,
47
+ user_id=user_id
48
+ )
49
+
50
+ return curriculum_id
51
+
52
+ async def generate_content_for_lesson(
53
+ self,
54
+ curriculum_id: str,
55
+ lesson_index: int,
56
+ lesson: Dict[str, Any],
57
+ metadata: Dict[str, Any]
58
+ ) -> Dict[str, str]:
59
+ """Generate all content types for a single lesson"""
60
+ content_ids = {}
61
+ lesson_topic = lesson.get('sub_topic', f'Lesson {lesson_index + 1}')
62
+ lesson_context = f"{lesson_topic}: {lesson.get('description', '')}"
63
+
64
+ # Generate flashcards
65
+ try:
66
+ flashcards_instructions = (
67
+ config.flashcard_mode_instructions
68
+ .replace("{native_language}", metadata['native_language'])
69
+ .replace("{target_language}", metadata['target_language'])
70
+ .replace("{proficiency}", metadata['proficiency'])
71
+ )
72
+
73
+ flashcards_response = await api_cache.get_or_set(
74
+ category="flashcards",
75
+ key_text=lesson_context,
76
+ coro=generate_completions.get_completions,
77
+ context={
78
+ 'native_language': metadata['native_language'],
79
+ 'target_language': metadata['target_language'],
80
+ 'proficiency': metadata['proficiency'],
81
+ 'lesson_index': lesson_index
82
+ },
83
+ prompt=lesson_context,
84
+ instructions=flashcards_instructions
85
+ )
86
+
87
+ # Save flashcards
88
+ content_ids['flashcards'] = await db.save_learning_content(
89
+ curriculum_id=curriculum_id,
90
+ content_type='flashcards',
91
+ lesson_index=lesson_index,
92
+ lesson_topic=lesson_topic,
93
+ content=flashcards_response
94
+ )
95
+ except Exception as e:
96
+ logger.error(f"Failed to generate flashcards for lesson {lesson_index}: {e}")
97
+
98
+ # Generate exercises
99
+ try:
100
+ exercises_instructions = (
101
+ config.exercise_mode_instructions
102
+ .replace("{native_language}", metadata['native_language'])
103
+ .replace("{target_language}", metadata['target_language'])
104
+ .replace("{proficiency}", metadata['proficiency'])
105
+ )
106
+
107
+ exercises_response = await api_cache.get_or_set(
108
+ category="exercises",
109
+ key_text=lesson_context,
110
+ coro=generate_completions.get_completions,
111
+ context={
112
+ 'native_language': metadata['native_language'],
113
+ 'target_language': metadata['target_language'],
114
+ 'proficiency': metadata['proficiency'],
115
+ 'lesson_index': lesson_index
116
+ },
117
+ prompt=lesson_context,
118
+ instructions=exercises_instructions
119
+ )
120
+
121
+ # Save exercises
122
+ content_ids['exercises'] = await db.save_learning_content(
123
+ curriculum_id=curriculum_id,
124
+ content_type='exercises',
125
+ lesson_index=lesson_index,
126
+ lesson_topic=lesson_topic,
127
+ content=exercises_response
128
+ )
129
+ except Exception as e:
130
+ logger.error(f"Failed to generate exercises for lesson {lesson_index}: {e}")
131
+
132
+ # Generate simulation
133
+ try:
134
+ simulation_instructions = (
135
+ config.simulation_mode_instructions
136
+ .replace("{native_language}", metadata['native_language'])
137
+ .replace("{target_language}", metadata['target_language'])
138
+ .replace("{proficiency}", metadata['proficiency'])
139
+ )
140
+
141
+ simulation_response = await api_cache.get_or_set(
142
+ category="simulation",
143
+ key_text=lesson_context,
144
+ coro=generate_completions.get_completions,
145
+ context={
146
+ 'native_language': metadata['native_language'],
147
+ 'target_language': metadata['target_language'],
148
+ 'proficiency': metadata['proficiency'],
149
+ 'lesson_index': lesson_index
150
+ },
151
+ prompt=lesson_context,
152
+ instructions=simulation_instructions
153
+ )
154
+
155
+ # Save simulation
156
+ content_ids['simulation'] = await db.save_learning_content(
157
+ curriculum_id=curriculum_id,
158
+ content_type='simulation',
159
+ lesson_index=lesson_index,
160
+ lesson_topic=lesson_topic,
161
+ content=simulation_response
162
+ )
163
+ except Exception as e:
164
+ logger.error(f"Failed to generate simulation for lesson {lesson_index}: {e}")
165
+
166
+ return content
167
+
168
+ async def generate_all_content_for_curriculum(
169
+ self,
170
+ curriculum_id: str,
171
+ max_concurrent_lessons: int = 3
172
+ ):
173
+ """Generate all learning content for a curriculum"""
174
+ # Get curriculum details
175
+ curriculum_data = await db.get_curriculum(curriculum_id)
176
+ if not curriculum_data:
177
+ logger.error(f"Curriculum not found: {curriculum_id}")
178
+ return
179
+
180
+ # Parse curriculum JSON
181
+ try:
182
+ curriculum = json.loads(curriculum_data['curriculum_json'])
183
+ lessons = curriculum.get('sub_topics', [])
184
+ except json.JSONDecodeError:
185
+ logger.error(f"Failed to parse curriculum JSON for {curriculum_id}")
186
+ return
187
+
188
+ # Prepare metadata
189
+ metadata = {
190
+ 'native_language': curriculum_data['native_language'],
191
+ 'target_language': curriculum_data['target_language'],
192
+ 'proficiency': curriculum_data['proficiency']
193
+ }
194
+
195
+ logger.info(f"Starting content generation for {len(lessons)} lessons")
196
+
197
+ # Process lessons in batches to avoid overwhelming the API
198
+ for i in range(0, len(lessons), max_concurrent_lessons):
199
+ batch = lessons[i:i + max_concurrent_lessons]
200
+ batch_indices = list(range(i, min(i + max_concurrent_lessons, len(lessons))))
201
+
202
+ # Generate content for batch concurrently
203
+ tasks = [
204
+ self.generate_content_for_lesson(
205
+ curriculum_id=curriculum_id,
206
+ lesson_index=idx,
207
+ lesson=lesson,
208
+ metadata=metadata
209
+ )
210
+ for idx, lesson in zip(batch_indices, batch)
211
+ ]
212
+
213
+ results = await asyncio.gather(*tasks, return_exceptions=True)
214
+
215
+ for idx, result in zip(batch_indices, results):
216
+ if isinstance(result, Exception):
217
+ logger.error(f"Failed to generate content for lesson {idx}: {result}")
218
+ else:
219
+ logger.info(f"Generated content for lesson {idx}: {result}")
220
+
221
+ # Mark curriculum as content generated
222
+ await db.mark_curriculum_content_generated(curriculum_id)
223
+ logger.info(f"Completed content generation for curriculum {curriculum_id}")
224
+
225
+ async def process_metadata_extraction(
226
+ self,
227
+ extraction_id: str,
228
+ query: str,
229
+ metadata: Dict[str, Any],
230
+ user_id: Optional[int] = None,
231
+ generate_content: bool = True
232
+ ) -> Dict[str, Any]:
233
+ """Process a metadata extraction by checking for existing curriculum or generating new one"""
234
+
235
+ # Check for existing curriculum first
236
+ existing_curriculum = await db.find_existing_curriculum(
237
+ query=query,
238
+ native_language=metadata['native_language'],
239
+ target_language=metadata['target_language'],
240
+ proficiency=metadata['proficiency'],
241
+ user_id=user_id
242
+ )
243
+
244
+ if existing_curriculum:
245
+ # If we found an exact match for this user, return it
246
+ if existing_curriculum.get('user_id') == user_id:
247
+ logger.info(f"Found existing curriculum for user {user_id}: {existing_curriculum['id']}")
248
+ return {
249
+ 'curriculum_id': existing_curriculum['id'],
250
+ 'content_generation_started': False,
251
+ 'cached': True,
252
+ 'cache_type': 'user_exact_match'
253
+ }
254
+
255
+ # If we found a similar curriculum from another user, copy it
256
+ elif existing_curriculum.get('is_content_generated') == 1:
257
+ logger.info(f"Copying existing curriculum {existing_curriculum['id']} for user {user_id}")
258
+ curriculum_id = await db.copy_curriculum_for_user(
259
+ source_curriculum_id=existing_curriculum['id'],
260
+ metadata_extraction_id=extraction_id,
261
+ user_id=user_id
262
+ )
263
+ return {
264
+ 'curriculum_id': curriculum_id,
265
+ 'content_generation_started': False,
266
+ 'cached': True,
267
+ 'cache_type': 'copied_from_similar'
268
+ }
269
+
270
+ # No suitable existing curriculum found, generate new one
271
+ logger.info(f"No existing curriculum found, generating new one for user {user_id}")
272
+ curriculum_id = await self.generate_curriculum_from_metadata(
273
+ metadata_extraction_id=extraction_id,
274
+ query=query,
275
+ metadata=metadata,
276
+ user_id=user_id
277
+ )
278
+
279
+ result = {
280
+ 'curriculum_id': curriculum_id,
281
+ 'content_generation_started': False,
282
+ 'cached': False,
283
+ 'cache_type': 'newly_generated'
284
+ }
285
+
286
+ if generate_content:
287
+ # Start content generation in background
288
+ asyncio.create_task(self.generate_all_content_for_curriculum(curriculum_id))
289
+ result['content_generation_started'] = True
290
+
291
+ return result
292
+
293
+
294
+ # Global content generator instance
295
+ content_generator = ContentGenerator()
prev_backend_v4/backend/database_init.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Database initialization script for AI Language Tutor
4
+ Run this script to create database tables
5
+ """
6
+
7
+ import asyncio
8
+ import sys
9
+ import os
10
+
11
+ # Add the project root to Python path
12
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ from backend.database import create_tables, drop_tables
15
+ import logging
16
+
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ async def init_database():
22
+ """Initialize database tables"""
23
+ try:
24
+ logger.info("Creating database tables...")
25
+ await create_tables()
26
+ logger.info("Database tables created successfully!")
27
+ except Exception as e:
28
+ logger.error(f"Error creating database tables: {e}")
29
+ raise
30
+
31
+
32
+ async def reset_database():
33
+ """Reset database (drop and recreate tables)"""
34
+ try:
35
+ logger.info("Dropping existing tables...")
36
+ await drop_tables()
37
+ logger.info("Creating new tables...")
38
+ await create_tables()
39
+ logger.info("Database reset successfully!")
40
+ except Exception as e:
41
+ logger.error(f"Error resetting database: {e}")
42
+ raise
43
+
44
+
45
+ if __name__ == "__main__":
46
+ import argparse
47
+
48
+ parser = argparse.ArgumentParser(description="Database initialization for AI Language Tutor")
49
+ parser.add_argument(
50
+ "--reset",
51
+ action="store_true",
52
+ help="Reset database (drop and recreate tables)"
53
+ )
54
+
55
+ args = parser.parse_args()
56
+
57
+ if args.reset:
58
+ print("⚠️ WARNING: This will delete all existing data!")
59
+ confirm = input("Are you sure you want to reset the database? (yes/no): ")
60
+ if confirm.lower() == "yes":
61
+ asyncio.run(reset_database())
62
+ else:
63
+ print("Database reset cancelled.")
64
+ else:
65
+ asyncio.run(init_database())
prev_backend_v4/backend/db.py ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiosqlite
2
+ import json
3
+ import os
4
+ from typing import Optional, List, Dict, Any
5
+ from datetime import datetime
6
+ import uuid
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Database file path
12
+ DB_PATH = os.getenv("DATABASE_PATH", "./ai_tutor.db")
13
+
14
+
15
+ class Database:
16
+ """Pure SQLite database handler for AI Language Tutor"""
17
+
18
+ def __init__(self, db_path: str = DB_PATH):
19
+ self.db_path = db_path
20
+
21
+ async def initialize(self):
22
+ """Initialize database with schema"""
23
+ async with aiosqlite.connect(self.db_path) as db:
24
+ # Read and execute schema - look for it in parent directory
25
+ schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
26
+ with open(schema_path, 'r') as f:
27
+ schema = f.read()
28
+ await db.executescript(schema)
29
+ await db.commit()
30
+ logger.info("Database initialized successfully")
31
+
32
+ async def find_existing_curriculum(
33
+ self,
34
+ query: str,
35
+ native_language: str,
36
+ target_language: str,
37
+ proficiency: str,
38
+ user_id: Optional[int] = None
39
+ ) -> Optional[Dict[str, Any]]:
40
+ """Find existing curriculum for similar query and metadata"""
41
+ async with aiosqlite.connect(self.db_path) as db:
42
+ db.row_factory = aiosqlite.Row
43
+
44
+ if user_id is not None:
45
+ # User-specific search: First try to find exact query match for the user
46
+ async with db.execute("""
47
+ SELECT c.*, m.native_language, m.target_language, m.proficiency, m.title, m.query
48
+ FROM curricula c
49
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
50
+ WHERE m.user_id = ? AND m.query = ? AND m.native_language = ?
51
+ AND m.target_language = ? AND m.proficiency = ?
52
+ ORDER BY c.created_at DESC
53
+ LIMIT 1
54
+ """, (user_id, query, native_language, target_language, proficiency)) as cursor:
55
+ row = await cursor.fetchone()
56
+ if row:
57
+ return dict(row)
58
+
59
+ # Then try to find similar curriculum with same metadata (any user)
60
+ async with db.execute("""
61
+ SELECT c.*, m.native_language, m.target_language, m.proficiency, m.title, m.query
62
+ FROM curricula c
63
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
64
+ WHERE m.native_language = ? AND m.target_language = ? AND m.proficiency = ?
65
+ AND c.is_content_generated = 1
66
+ ORDER BY c.created_at DESC
67
+ LIMIT 1
68
+ """, (native_language, target_language, proficiency)) as cursor:
69
+ row = await cursor.fetchone()
70
+ if row:
71
+ return dict(row)
72
+ else:
73
+ # User-independent search: Find exact query match regardless of user
74
+ async with db.execute("""
75
+ SELECT c.*, m.native_language, m.target_language, m.proficiency, m.title, m.query
76
+ FROM curricula c
77
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
78
+ WHERE m.query = ? AND m.native_language = ? AND m.target_language = ? AND m.proficiency = ?
79
+ ORDER BY c.created_at DESC
80
+ LIMIT 1
81
+ """, (query, native_language, target_language, proficiency)) as cursor:
82
+ row = await cursor.fetchone()
83
+ if row:
84
+ return dict(row)
85
+
86
+ return None
87
+
88
+ async def save_metadata_extraction(
89
+ self,
90
+ query: str,
91
+ metadata: Dict[str, Any],
92
+ user_id: Optional[int] = None
93
+ ) -> str:
94
+ """Save extracted metadata and return extraction ID"""
95
+ extraction_id = str(uuid.uuid4())
96
+
97
+ async with aiosqlite.connect(self.db_path) as db:
98
+ await db.execute("""
99
+ INSERT INTO metadata_extractions
100
+ (id, user_id, query, native_language, target_language, proficiency, title, description, metadata_json)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
102
+ """, (
103
+ extraction_id,
104
+ user_id,
105
+ query,
106
+ metadata.get('native_language'),
107
+ metadata.get('target_language'),
108
+ metadata.get('proficiency'),
109
+ metadata.get('title'),
110
+ metadata.get('description'),
111
+ json.dumps(metadata)
112
+ ))
113
+ await db.commit()
114
+
115
+ logger.info(f"Saved metadata extraction: {extraction_id}")
116
+ return extraction_id
117
+
118
+ async def save_curriculum(
119
+ self,
120
+ metadata_extraction_id: str,
121
+ curriculum: Dict[str, Any],
122
+ user_id: Optional[int] = None
123
+ ) -> str:
124
+ """Save generated curriculum and return curriculum ID"""
125
+ curriculum_id = str(uuid.uuid4())
126
+
127
+ async with aiosqlite.connect(self.db_path) as db:
128
+ await db.execute("""
129
+ INSERT INTO curricula
130
+ (id, metadata_extraction_id, user_id, lesson_topic, curriculum_json)
131
+ VALUES (?, ?, ?, ?, ?)
132
+ """, (
133
+ curriculum_id,
134
+ metadata_extraction_id,
135
+ user_id,
136
+ curriculum.get('lesson_topic', ''),
137
+ json.dumps(curriculum)
138
+ ))
139
+ await db.commit()
140
+
141
+ logger.info(f"Saved curriculum: {curriculum_id}")
142
+ return curriculum_id
143
+
144
+ async def copy_curriculum_for_user(
145
+ self,
146
+ source_curriculum_id: str,
147
+ metadata_extraction_id: str,
148
+ user_id: Optional[int] = None
149
+ ) -> str:
150
+ """Copy an existing curriculum for a new user"""
151
+ new_curriculum_id = str(uuid.uuid4())
152
+
153
+ async with aiosqlite.connect(self.db_path) as db:
154
+ # Get source curriculum
155
+ async with db.execute("""
156
+ SELECT lesson_topic, curriculum_json FROM curricula WHERE id = ?
157
+ """, (source_curriculum_id,)) as cursor:
158
+ row = await cursor.fetchone()
159
+ if not row:
160
+ raise ValueError(f"Source curriculum {source_curriculum_id} not found")
161
+
162
+ lesson_topic, curriculum_json = row
163
+
164
+ # Create new curriculum
165
+ await db.execute("""
166
+ INSERT INTO curricula
167
+ (id, metadata_extraction_id, user_id, lesson_topic, curriculum_json, is_content_generated)
168
+ VALUES (?, ?, ?, ?, ?, 0)
169
+ """, (
170
+ new_curriculum_id,
171
+ metadata_extraction_id,
172
+ user_id,
173
+ lesson_topic,
174
+ curriculum_json
175
+ ))
176
+
177
+ # Copy all learning content
178
+ await db.execute("""
179
+ INSERT INTO learning_content
180
+ (id, curriculum_id, content_type, lesson_index, lesson_topic, content_json)
181
+ SELECT
182
+ lower(hex(randomblob(16))),
183
+ ?,
184
+ content_type,
185
+ lesson_index,
186
+ lesson_topic,
187
+ content_json
188
+ FROM learning_content
189
+ WHERE curriculum_id = ?
190
+ """, (new_curriculum_id, source_curriculum_id))
191
+
192
+ # Mark as content generated
193
+ await db.execute("""
194
+ UPDATE curricula
195
+ SET is_content_generated = 1
196
+ WHERE id = ?
197
+ """, (new_curriculum_id,))
198
+
199
+ await db.commit()
200
+
201
+ logger.info(f"Copied curriculum {source_curriculum_id} to {new_curriculum_id} for user {user_id}")
202
+ return new_curriculum_id
203
+
204
+ async def save_learning_content(
205
+ self,
206
+ curriculum_id: str,
207
+ content_type: str,
208
+ lesson_index: int,
209
+ lesson_topic: str,
210
+ content: Any
211
+ ) -> str:
212
+ """Save learning content (flashcards, exercises, or simulation)"""
213
+ content_id = str(uuid.uuid4())
214
+
215
+ async with aiosqlite.connect(self.db_path) as db:
216
+ await db.execute("""
217
+ INSERT INTO learning_content
218
+ (id, curriculum_id, content_type, lesson_index, lesson_topic, content_json)
219
+ VALUES (?, ?, ?, ?, ?, ?)
220
+ """, (
221
+ content_id,
222
+ curriculum_id,
223
+ content_type,
224
+ lesson_index,
225
+ lesson_topic,
226
+ json.dumps(content) if isinstance(content, (dict, list)) else content
227
+ ))
228
+ await db.commit()
229
+
230
+ logger.info(f"Saved {content_type} for lesson {lesson_index}")
231
+ return content_id
232
+
233
+ async def mark_curriculum_content_generated(self, curriculum_id: str):
234
+ """Mark curriculum as having all content generated"""
235
+ async with aiosqlite.connect(self.db_path) as db:
236
+ await db.execute("""
237
+ UPDATE curricula
238
+ SET is_content_generated = 1
239
+ WHERE id = ?
240
+ """, (curriculum_id,))
241
+ await db.commit()
242
+
243
+ async def get_metadata_extraction(self, extraction_id: str) -> Optional[Dict[str, Any]]:
244
+ """Get metadata extraction by ID"""
245
+ async with aiosqlite.connect(self.db_path) as db:
246
+ db.row_factory = aiosqlite.Row
247
+ async with db.execute("""
248
+ SELECT * FROM metadata_extractions WHERE id = ?
249
+ """, (extraction_id,)) as cursor:
250
+ row = await cursor.fetchone()
251
+ if row:
252
+ return dict(row)
253
+ return None
254
+
255
+ async def get_curriculum(self, curriculum_id: str) -> Optional[Dict[str, Any]]:
256
+ """Get curriculum by ID"""
257
+ async with aiosqlite.connect(self.db_path) as db:
258
+ db.row_factory = aiosqlite.Row
259
+ async with db.execute("""
260
+ SELECT c.*, m.native_language, m.target_language, m.proficiency
261
+ FROM curricula c
262
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
263
+ WHERE c.id = ?
264
+ """, (curriculum_id,)) as cursor:
265
+ row = await cursor.fetchone()
266
+ if row:
267
+ return dict(row)
268
+ return None
269
+
270
+ async def get_learning_content(
271
+ self,
272
+ curriculum_id: str,
273
+ content_type: Optional[str] = None,
274
+ lesson_index: Optional[int] = None
275
+ ) -> List[Dict[str, Any]]:
276
+ """Get learning content for a curriculum"""
277
+ query = "SELECT * FROM learning_content WHERE curriculum_id = ?"
278
+ params = [curriculum_id]
279
+
280
+ if content_type:
281
+ query += " AND content_type = ?"
282
+ params.append(content_type)
283
+
284
+ if lesson_index is not None:
285
+ query += " AND lesson_index = ?"
286
+ params.append(lesson_index)
287
+
288
+ query += " ORDER BY lesson_index"
289
+
290
+ async with aiosqlite.connect(self.db_path) as db:
291
+ db.row_factory = aiosqlite.Row
292
+ async with db.execute(query, params) as cursor:
293
+ rows = await cursor.fetchall()
294
+ return [dict(row) for row in rows]
295
+
296
+ async def get_user_metadata_extractions(
297
+ self,
298
+ user_id: int,
299
+ limit: int = 20
300
+ ) -> List[Dict[str, Any]]:
301
+ """Get user's metadata extraction history"""
302
+ async with aiosqlite.connect(self.db_path) as db:
303
+ db.row_factory = aiosqlite.Row
304
+ async with db.execute("""
305
+ SELECT * FROM metadata_extractions
306
+ WHERE user_id = ?
307
+ ORDER BY created_at DESC
308
+ LIMIT ?
309
+ """, (user_id, limit)) as cursor:
310
+ rows = await cursor.fetchall()
311
+ return [dict(row) for row in rows]
312
+
313
+ async def get_user_curricula(
314
+ self,
315
+ user_id: int,
316
+ limit: int = 20
317
+ ) -> List[Dict[str, Any]]:
318
+ """Get user's curricula"""
319
+ async with aiosqlite.connect(self.db_path) as db:
320
+ db.row_factory = aiosqlite.Row
321
+ async with db.execute("""
322
+ SELECT c.*, m.native_language, m.target_language, m.proficiency, m.title
323
+ FROM curricula c
324
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
325
+ WHERE c.user_id = ?
326
+ ORDER BY c.created_at DESC
327
+ LIMIT ?
328
+ """, (user_id, limit)) as cursor:
329
+ rows = await cursor.fetchall()
330
+ return [dict(row) for row in rows]
331
+
332
+ async def get_user_learning_journeys(
333
+ self,
334
+ user_id: int,
335
+ limit: int = 20
336
+ ) -> List[Dict[str, Any]]:
337
+ """Get user's complete learning journeys"""
338
+ async with aiosqlite.connect(self.db_path) as db:
339
+ db.row_factory = aiosqlite.Row
340
+ async with db.execute("""
341
+ SELECT * FROM user_learning_journeys
342
+ WHERE user_id = ?
343
+ LIMIT ?
344
+ """, (user_id, limit)) as cursor:
345
+ rows = await cursor.fetchall()
346
+ return [dict(row) for row in rows]
347
+
348
+ async def get_curriculum_content_status(self, curriculum_id: str) -> Optional[Dict[str, Any]]:
349
+ """Get content generation status for a curriculum"""
350
+ async with aiosqlite.connect(self.db_path) as db:
351
+ db.row_factory = aiosqlite.Row
352
+ async with db.execute("""
353
+ SELECT * FROM curriculum_content_status WHERE curriculum_id = ?
354
+ """, (curriculum_id,)) as cursor:
355
+ row = await cursor.fetchone()
356
+ if row:
357
+ return dict(row)
358
+ return None
359
+
360
+ async def get_full_curriculum_details(self, curriculum_id: str, include_content: bool = True) -> Optional[Dict[str, Any]]:
361
+ """Get full curriculum details, optionally including all content."""
362
+ curriculum = await self.get_curriculum(curriculum_id)
363
+ if not curriculum:
364
+ return None
365
+
366
+ try:
367
+ curriculum_data = json.loads(curriculum['curriculum_json'])
368
+ lessons = curriculum_data.get('sub_topics', [])
369
+ except json.JSONDecodeError:
370
+ curriculum_data = {}
371
+ lessons = []
372
+
373
+ if include_content:
374
+ content_list = await self.get_learning_content(curriculum_id)
375
+ content_map = {}
376
+ for content in content_list:
377
+ lesson_index = content['lesson_index']
378
+ content_type = content['content_type']
379
+ if lesson_index not in content_map:
380
+ content_map[lesson_index] = {}
381
+
382
+ try:
383
+ parsed_content = json.loads(content['content_json'])
384
+ except json.JSONDecodeError:
385
+ parsed_content = content['content_json']
386
+
387
+ content_map[lesson_index][content_type] = {
388
+ "id": content['id'],
389
+ "lesson_topic": content['lesson_topic'],
390
+ "content": parsed_content,
391
+ "created_at": content['created_at']
392
+ }
393
+
394
+ # Embed content into lessons
395
+ for i, lesson in enumerate(lessons):
396
+ lesson['content'] = content_map.get(i, {})
397
+
398
+ curriculum['curriculum'] = curriculum_data
399
+ del curriculum['curriculum_json']
400
+
401
+ return curriculum
402
+
403
+ async def search_curricula_by_languages(
404
+ self,
405
+ native_language: str,
406
+ target_language: str,
407
+ proficiency: Optional[str] = None,
408
+ limit: int = 10
409
+ ) -> List[Dict[str, Any]]:
410
+ """Search for existing curricula by language combination"""
411
+ query = """
412
+ SELECT c.*, m.native_language, m.target_language, m.proficiency, m.title
413
+ FROM curricula c
414
+ JOIN metadata_extractions m ON c.metadata_extraction_id = m.id
415
+ WHERE m.native_language = ? AND m.target_language = ?
416
+ """
417
+ params = [native_language, target_language]
418
+
419
+ if proficiency:
420
+ query += " AND m.proficiency = ?"
421
+ params.append(proficiency)
422
+
423
+ query += " ORDER BY c.created_at DESC LIMIT ?"
424
+ params.append(limit)
425
+
426
+ async with aiosqlite.connect(self.db_path) as db:
427
+ db.row_factory = aiosqlite.Row
428
+ async with db.execute(query, params) as cursor:
429
+ rows = await cursor.fetchall()
430
+ return [dict(row) for row in rows]
431
+
432
+
433
+ # Global database instance
434
+ db = Database()