Jakaria commited on
Commit
7aa41d1
Β·
1 Parent(s): f2f3f1e

commit initial code

Browse files
Files changed (3) hide show
  1. Dockerfile +16 -0
  2. main.py +384 -0
  3. requirements.txt +7 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LangGraph Japanese Lesson Generator with Structured Output
3
+ Two-Agent System with Native Structure Output from LLM
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.responses import JSONResponse
8
+ from pydantic import BaseModel, Field
9
+ from typing import Optional, List
10
+ import os
11
+ from langchain_groq import ChatGroq
12
+ from langgraph.graph import StateGraph, END
13
+ from typing import TypedDict
14
+ from dotenv import load_dotenv
15
+
16
+ # ============ LOAD ENVIRONMENT ============
17
+ load_dotenv()
18
+
19
+ # ============ PYDANTIC BASE MODELS ============
20
+
21
+ class VocabularyItem(BaseModel):
22
+ """Vocabulary item with all required fields"""
23
+ japanese: str = Field(..., description="Japanese word in Kanji/Hiragana/Katakana")
24
+ romaji: str = Field(..., description="Romanized version of the word")
25
+ meaning: str = Field(..., description="English meaning of the word")
26
+ example: str = Field(..., description="Example sentence using the word")
27
+
28
+
29
+ class GrammarPoint(BaseModel):
30
+ """Grammar point with all required fields"""
31
+ pattern: str = Field(..., description="The grammar pattern (e.g., N1 は N2 です)")
32
+ explanation: str = Field(..., description="Detailed explanation of the pattern")
33
+ example: str = Field(..., description="Example sentence showing the pattern")
34
+ translation: str = Field(..., description="English translation of the example")
35
+
36
+
37
+ class Question(BaseModel):
38
+ """Multiple choice question with all required fields"""
39
+ question: str = Field(..., description="The question text")
40
+ options: List[str] = Field(..., min_items=4, max_items=4, description="Exactly 4 answer options")
41
+ correctIndex: int = Field(..., ge=0, le=3, description="Index of correct answer (0-3)")
42
+
43
+
44
+ class SubSection(BaseModel):
45
+ """Subsection with conversation, vocabulary, grammar, and questions"""
46
+ title: str = Field(..., description="Title of the subsection")
47
+ conversation: str = Field(..., description="Dialog conversation in format 'A: text\\nB: text'")
48
+ vocabularies: List[VocabularyItem] = Field(..., min_items=5, description="At least 5 vocabulary items")
49
+ grammarPoints: List[GrammarPoint] = Field(..., min_items=3, description="At least 3 grammar points")
50
+ questions: List[Question] = Field(..., min_items=4, max_items=5, description="4-5 questions")
51
+
52
+
53
+ class Lesson(BaseModel):
54
+ """Complete lesson with all required sections"""
55
+ title: str = Field(..., description="Lesson title (English and Japanese)")
56
+ description: str = Field(..., description="Comprehensive lesson description")
57
+ color: str = Field(..., pattern="^#[0-9a-fA-F]{6}$", description="Hex color code")
58
+ subSections: List[SubSection] = Field(..., min_items=2, max_items=3, description="2-3 subsections")
59
+
60
+
61
+ class OrganizedData(BaseModel):
62
+ """Output from organizer agent"""
63
+ lessonTitle: str = Field(..., description="Title of the lesson")
64
+ lessonDescription: str = Field(..., description="Description of the lesson")
65
+ subSections: List[dict] = Field(..., description="List of subsections to create")
66
+
67
+
68
+ class LessonRequest(BaseModel):
69
+ """API request model"""
70
+ lesson_topic: str = Field(..., description="The lesson topic to generate")
71
+ additional_context: Optional[str] = Field(None, description="Additional context or requirements")
72
+
73
+
74
+ # ============ STATE DEFINITIONS ============
75
+ class State(TypedDict):
76
+ raw_input: str
77
+ organized_data: Optional[OrganizedData]
78
+ final_lesson: Optional[Lesson]
79
+ error: Optional[str]
80
+
81
+
82
+ # ============ INITIALIZE LLM WITH STRUCTURE OUTPUT ============
83
+ app = FastAPI()
84
+
85
+ base_llm = ChatGroq(
86
+ model="llama-3.3-70b-versatile",
87
+ temperature=0.7,
88
+ api_key=os.getenv("GROQ_API_KEY")
89
+ )
90
+
91
+ # Create structured output versions
92
+ organizer_llm = base_llm.with_structured_output(OrganizedData)
93
+ generator_llm = base_llm.with_structured_output(Lesson)
94
+
95
+
96
+ # ============ AGENT 1: ORGANIZER AGENT ============
97
+ def organizer_agent(state: State) -> State:
98
+ """
99
+ First Agent: Organizes unstructured input data
100
+ Uses structured output to guarantee proper format
101
+ """
102
+
103
+ organize_prompt = f"""You are a Japanese lesson content organizer.
104
+ Your job is to take unstructured user input about a Japanese lesson topic and organize it logically.
105
+
106
+ User Input: {state['raw_input']}
107
+
108
+ Analyze this and organize into a structured format:
109
+
110
+ Requirements:
111
+ 1. Extract or create a clear Lesson Title
112
+ 2. Write a comprehensive Lesson Description (2-3 sentences)
113
+ 3. Determine 2-3 SubSections (typically progression from basic to complex)
114
+ 4. For each subsection, identify:
115
+ - Clear title
116
+ - Main topics/themes to cover
117
+ - Vocabulary themes (e.g., greetings, family, food)
118
+ - Relevant grammar patterns
119
+ - Number of questions (4-5)
120
+
121
+ Think pedagogically about lesson progression. Be specific and detailed."""
122
+
123
+ try:
124
+ print("πŸ”„ Organizer Agent processing...")
125
+ organized_result = organizer_llm.invoke(organize_prompt)
126
+ state["organized_data"] = organized_result
127
+ print("βœ… Organizer Agent completed successfully")
128
+ return state
129
+ except Exception as e:
130
+ state["error"] = f"Organizer Agent Error: {str(e)}"
131
+ print(f"❌ Organizer Error: {e}")
132
+ return state
133
+
134
+
135
+ # ============ AGENT 2: GENERATOR AGENT ============
136
+ def generator_agent(state: State) -> State:
137
+ """
138
+ Second Agent: Generates complete lesson content
139
+ Uses structured output to guarantee exact schema compliance
140
+ """
141
+
142
+ if state["error"]:
143
+ return state
144
+
145
+ organized_data = state["organized_data"]
146
+
147
+ generate_prompt = f"""You are an expert Japanese language teacher creating authentic, complete lesson content.
148
+
149
+ Based on this lesson structure:
150
+ Title: {organized_data.lessonTitle}
151
+ Description: {organized_data.lessonDescription}
152
+
153
+ SubSections needed:
154
+ {chr(10).join([f"- {sub['title']}: {sub.get('topics', [])} | Vocabulary: {sub.get('vocabularyThemes', [])} | Grammar: {sub.get('grammarPatterns', [])} | Questions: {sub.get('questionCount', 4)}" for sub in organized_data.subSections])}
155
+
156
+ Generate a COMPLETE Japanese lesson with:
157
+
158
+ 1. Title: Clear lesson name in English and Japanese
159
+ 2. Description: 2-3 sentence comprehensive overview
160
+ 3. Color: Choose one from: #3498db, #e74c3c, #2ecc71, #f39c12, #9b59b6, #1abc9c, #e91e63, #00bcd4
161
+ 4. Create 2-3 subsections as specified
162
+
163
+ FOR EACH SUBSECTION:
164
+ - Title: Clear subsection name
165
+ - Conversation: MUST be natural Japanese dialogue. Format: "A: [Japanese]\\nB: [Japanese]\\nA: [Japanese]\\nB: [Japanese]"
166
+ (Include at least 4 lines. Use REAL Japanese sentences.)
167
+ - Vocabularies: Minimum 5-8 items per subsection
168
+ * japanese: ONLY Kanji/Hiragana/Katakana (e.g., "こんにけは")
169
+ * romaji: Romanized (e.g., "Konnichiwa")
170
+ * meaning: English translation
171
+ * example: Example sentence using the word
172
+ - grammarPoints: EXACTLY 3-4 grammar patterns
173
+ * pattern: Grammar rule (e.g., "N1 は N2 です")
174
+ * explanation: Clear explanation
175
+ * example: Japanese sentence demonstrating it
176
+ * translation: English translation
177
+ - questions: EXACTLY 4-5 multiple choice questions
178
+ * question: English question
179
+ * options: EXACTLY 4 answer choices
180
+ * correctIndex: Index of correct answer (0, 1, 2, or 3)
181
+
182
+ Create authentic, educational content. Every field must be filled with real, useful content."""
183
+
184
+ try:
185
+ print("πŸ”„ Generator Agent processing...")
186
+ lesson_result = generator_llm.invoke(generate_prompt)
187
+ state["final_lesson"] = lesson_result
188
+ print("βœ… Generator Agent completed successfully")
189
+ print(f"βœ… Generated lesson: {lesson_result.title}")
190
+ return state
191
+ except Exception as e:
192
+ state["error"] = f"Generator Agent Error: {str(e)}"
193
+ print(f"❌ Generator Error: {e}")
194
+ return state
195
+
196
+
197
+ # ============ BUILD LANGGRAPH ============
198
+ def build_graph():
199
+ """Create the LangGraph workflow"""
200
+ graph = StateGraph(State)
201
+
202
+ # Add nodes
203
+ graph.add_node("organizer", organizer_agent)
204
+ graph.add_node("generator", generator_agent)
205
+
206
+ # Add edges
207
+ graph.add_edge("organizer", "generator")
208
+ graph.add_edge("generator", END)
209
+
210
+ # Set entry point
211
+ graph.set_entry_point("organizer")
212
+
213
+ return graph.compile()
214
+
215
+
216
+ workflow = build_graph()
217
+
218
+
219
+ # ============ API ENDPOINT ============
220
+ @app.post("/api/generate-lesson")
221
+ async def generate_lesson(request: LessonRequest):
222
+ """
223
+ Main API endpoint to generate Japanese lessons
224
+
225
+ Input: Raw lesson topic/content
226
+ Process: Two-agent LangGraph workflow with structured output
227
+ Output: Strictly validated lesson JSON following exact schema
228
+ """
229
+
230
+ try:
231
+ if not request.lesson_topic or request.lesson_topic.strip() == "":
232
+ raise HTTPException(status_code=400, detail="lesson_topic cannot be empty")
233
+
234
+ print(f"\nπŸš€ Starting lesson generation for: {request.lesson_topic[:50]}...")
235
+
236
+ # Prepare initial state
237
+ initial_state: State = {
238
+ "raw_input": request.lesson_topic,
239
+ "organized_data": None,
240
+ "final_lesson": None,
241
+ "error": None
242
+ }
243
+
244
+ # Run the workflow
245
+ result = workflow.invoke(initial_state)
246
+
247
+ # Check for errors
248
+ if result.get("error"):
249
+ print(f"⚠️ Workflow Error: {result['error']}")
250
+ raise HTTPException(status_code=500, detail=result["error"])
251
+
252
+ if not result.get("final_lesson"):
253
+ raise HTTPException(status_code=500, detail="Failed to generate lesson")
254
+
255
+ print("βœ… Lesson generation completed successfully!")
256
+
257
+ # Return as JSON
258
+ return result["final_lesson"].model_dump()
259
+
260
+ except HTTPException:
261
+ raise
262
+ except Exception as e:
263
+ print(f"❌ API Error: {str(e)}")
264
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
265
+
266
+
267
+ # ============ HEALTH CHECK ENDPOINT ============
268
+ @app.get("/health")
269
+ async def health_check():
270
+ """Health check endpoint"""
271
+ return {
272
+ "status": "healthy",
273
+ "message": "LangGraph Lesson Generator with Structured Output is running"
274
+ }
275
+
276
+
277
+ # ============ EXAMPLE ENDPOINT ============
278
+ @app.get("/example")
279
+ async def example():
280
+ """Return example of expected output format"""
281
+ example_lesson = {
282
+ "title": "Self Introduction (γ˜γ“γ—γ‚‡γ†γ‹γ„)",
283
+ "description": "Learn greetings, how to introduce yourself, say your age and birthday, and talk about your family.",
284
+ "color": "#3498db",
285
+ "subSections": [
286
+ {
287
+ "title": "Hello & Basic Introduction",
288
+ "conversation": "A: こんにけは!\\nB: γ“γ‚“γ«γ‘γ―γ€‚γ―γ˜γ‚γΎγ—γ¦γ€‚\\nA: γ‚γŸγ—γ―γ‚­γƒ γ§γ™γ€‚γ‹γ‚“γ“γγ‹γ‚‰γγΎγ—γŸγ€‚\\nB: γ‚ˆγ‚γ—γγŠγ­γŒγ„γ—γΎγ™γ€‚",
289
+ "vocabularies": [
290
+ {
291
+ "japanese": "こんにけは",
292
+ "romaji": "Konnichiwa",
293
+ "meaning": "Hello / Good afternoon",
294
+ "example": "こんにけは!"
295
+ },
296
+ {
297
+ "japanese": "γ―γ˜γ‚γΎγ—γ¦",
298
+ "romaji": "Hajimemashite",
299
+ "meaning": "Nice to meet you (first time)",
300
+ "example": "γ―γ˜γ‚γΎγ—γ¦γ€‚"
301
+ },
302
+ {
303
+ "japanese": "γ‚γŸγ—",
304
+ "romaji": "Watashi",
305
+ "meaning": "I / Me",
306
+ "example": "γ‚γŸγ—γ―ε­¦η”Ÿγ§γ™γ€‚"
307
+ },
308
+ {
309
+ "japanese": "です",
310
+ "romaji": "Desu",
311
+ "meaning": "to be (polite)",
312
+ "example": "γ‚γŸγ—γ―ε­¦η”Ÿγ§γ™γ€‚"
313
+ },
314
+ {
315
+ "japanese": "γ‹γ‚‰γγΎγ—γŸ",
316
+ "romaji": "Kara kimashita",
317
+ "meaning": "Came from",
318
+ "example": "バングラデシγƒ₯γ‹γ‚‰γγΎγ—γŸγ€‚"
319
+ },
320
+ {
321
+ "japanese": "γ‚ˆγ‚γ—γγŠγ­γŒγ„γ—γΎγ™",
322
+ "romaji": "Yoroshiku onegaishimasu",
323
+ "meaning": "Please take care of me",
324
+ "example": "γ‚ˆγ‚γ—γγŠγ­γŒγ„γ—γΎγ™γ€‚"
325
+ },
326
+ {
327
+ "japanese": "かんこく",
328
+ "romaji": "Kankoku",
329
+ "meaning": "Korea",
330
+ "example": "γ‹γ‚“γ“γγ‹γ‚‰γγΎγ—γŸγ€‚"
331
+ }
332
+ ],
333
+ "grammarPoints": [
334
+ {
335
+ "pattern": "N は N です",
336
+ "explanation": "Used to state what something/someone is.",
337
+ "example": "γ‚γŸγ—γ―ε­¦η”Ÿγ§γ™γ€‚",
338
+ "translation": "I am a student."
339
+ },
340
+ {
341
+ "pattern": "N は N からζ₯γΎγ—γŸ",
342
+ "explanation": "Used to say where someone came from.",
343
+ "example": "γ‚γŸγ—γ―γƒγƒ³γ‚°γƒ©γƒ‡γ‚·γƒ₯γ‹γ‚‰γγΎγ—γŸγ€‚",
344
+ "translation": "I came from Bangladesh."
345
+ },
346
+ {
347
+ "pattern": "γ‚ˆγ‚γ—γγŠγ­γŒγ„γ—γΎγ™",
348
+ "explanation": "Polite greeting used when meeting someone for the first time.",
349
+ "example": "γ―γ˜γ‚γΎγ—γ¦γ€‚γ‚ˆγ‚γ—γγŠγ­γŒγ„γ—γΎγ™γ€‚",
350
+ "translation": "Nice to meet you. Please take care of me."
351
+ }
352
+ ],
353
+ "questions": [
354
+ {
355
+ "question": "How do you say 'Hello' in Japanese?",
356
+ "options": ["γ•γ‚ˆγ†γͺら", "こんにけは", "γŠγ―γ‚ˆγ†", "γŠγ‚„γ™γΏγͺさい"],
357
+ "correctIndex": 1
358
+ },
359
+ {
360
+ "question": "What does 'γ―γ˜γ‚γΎγ—γ¦' mean?",
361
+ "options": ["Goodbye", "Nice to meet you", "Thank you", "Good morning"],
362
+ "correctIndex": 1
363
+ },
364
+ {
365
+ "question": "How would you say you came from Bangladesh?",
366
+ "options": ["バングラデシγƒ₯です。", "バングラデシγƒ₯γ‹γ‚‰γγΎγ—γŸγ€‚", "バングラデシγƒ₯へいきます。", "バングラデシγƒ₯で働きます。"],
367
+ "correctIndex": 1
368
+ },
369
+ {
370
+ "question": "Which sentence is correct for 'I am a student'?",
371
+ "options": ["γ‚γŸγ—γŒγγ›οΏ½οΏ½γ§γ™γ€‚", "γ‚γŸγ—γ―ε­¦η”Ÿγ§γ™γ€‚", "ε­¦η”Ÿγ―γ‚γŸγ—γ§γ™γ€‚", "γ‚γŸγ—γ‚’ε­¦η”Ÿγ§γ™γ€‚"],
372
+ "correctIndex": 1
373
+ }
374
+ ]
375
+ }
376
+ ]
377
+ }
378
+ return example_lesson
379
+
380
+
381
+ # ============ RUN SERVER ============
382
+ if __name__ == "__main__":
383
+ import uvicorn
384
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ langchain
4
+ langchain-groq
5
+ pydantic
6
+ langgraph
7
+ python-dotenv