vikramvasudevan commited on
Commit
23f8dc9
·
verified ·
1 Parent(s): c32bc40
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to HF Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # deploy on pushes to main branch
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ # 1. Checkout repo
13
+ - uses: actions/checkout@v3
14
+
15
+ # 2. Set up Python
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v4
18
+ with:
19
+ python-version: '3.13.6'
20
+
21
+ # 3. Install dependencies
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install --upgrade huggingface_hub
26
+
27
+ # 4. Authenticate to HF
28
+ - name: Hugging Face login
29
+ env:
30
+ HF_TOKEN: ${{ secrets.hf_token }}
31
+ run: hf auth login --token ${HF_TOKEN}
32
+
33
+ # 5. Push to HF Space repo
34
+ - name: Push to HF Space
35
+ env:
36
+ HF_TOKEN: ${{ secrets.hf_token }}
37
+ run: |
38
+ hf upload --repo-type space --commit-message "CI deploy" vikramvasudevan/paatashaala ./
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pkl
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.13.6
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", "api.server:app", "--host", "0.0.0.0", "--port", "7860"]
Pipfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [[source]]
2
+ name = "pypi"
3
+ url = "https://pypi.org/simple"
4
+ verify_ssl = true
5
+
6
+ [dev-packages]
7
+
8
+ [packages]
9
+ fastapi = "*"
10
+ uvicorn = "*"
11
+ pydantic = "*"
12
+ requests = "*"
13
+ python-dotenv = "*"
Pipfile.lock ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_meta": {
3
+ "hash": {
4
+ "sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100"
5
+ },
6
+ "pipfile-spec": 6,
7
+ "requires": {
8
+ "python_version": "3.13"
9
+ },
10
+ "sources": [
11
+ {
12
+ "name": "pypi",
13
+ "url": "https://pypi.org/simple",
14
+ "verify_ssl": true
15
+ }
16
+ ]
17
+ },
18
+ "default": {},
19
+ "develop": {}
20
+ }
README.md CHANGED
@@ -1,11 +1,2 @@
1
- ---
2
- title: Paatashaala
3
- emoji: 🐢
4
- colorFrom: pink
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- short_description: 'An AI app that '
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Paatashaala Quiz API
2
+ This space serves a FastAPI-based quiz API.
 
 
 
 
 
 
 
 
 
api/models.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Dict
3
+
4
+ # -------------------------------
5
+ # Models
6
+ # -------------------------------
7
+
8
+ class QuizQuestion(BaseModel):
9
+ id: str
10
+ question: str
11
+ choices: List[str]
12
+ correctIndex: int
13
+ explanation: str
14
+
15
+ class QuizSessionResponse(BaseModel):
16
+ session_id: str
17
+ question_index: int
18
+ total_questions: int
19
+ question: Optional[QuizQuestion] = None
20
+ done: bool = False
21
+ message: Optional[str] = None
22
+ score : int = 0
23
+
24
+ class QuizSession:
25
+ def __init__(self, session_id: str, questions: List[QuizQuestion]):
26
+ self.session_id = session_id
27
+ self.questions = questions
28
+ self.current_index = 0
29
+ self.score = 0
30
+
31
+ class QuizRequest(BaseModel):
32
+ subject: str
33
+ grade: str
34
+
35
+ class AnswerPayload(BaseModel):
36
+ choiceIndex: int
37
+
38
+ class AnswerResult(BaseModel):
39
+ is_correct: bool
40
+ correctIndex: int
41
+ explanation: str
42
+ next_question_available: bool
43
+ score: int
44
+
45
+
46
+ class SubjectsResponse(BaseModel):
47
+ subjects: List[str]
48
+
49
+
50
+ class SubjectListResponse(BaseModel):
51
+ grade: str
52
+ subjects: List[str] = Field(..., description="List of subjects appropriate for the given grade")
api/server.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # quiz_app.py
2
+ import uuid
3
+ from fastapi import FastAPI
4
+ from typing import Dict
5
+
6
+ from api.models import (
7
+ AnswerPayload,
8
+ AnswerResult,
9
+ QuizRequest,
10
+ QuizSession,
11
+ QuizSessionResponse,
12
+ )
13
+ from api.utils.quiz_helper import generate_questions, get_subjects_for_grade
14
+
15
+ app = FastAPI()
16
+
17
+ # -------------------------------
18
+ # In-memory storage
19
+ # -------------------------------
20
+ SESSIONS: Dict[str, QuizSession] = {}
21
+
22
+
23
+ # -------------------------------
24
+ # Routes
25
+ # -------------------------------
26
+
27
+
28
+ @app.post("/quiz_session/start_quiz", response_model=QuizSessionResponse)
29
+ def start_quiz(req: QuizRequest):
30
+ questions = generate_questions(req.subject, req.grade, count=3)
31
+ session_id = str(uuid.uuid4())
32
+ session = QuizSession(session_id, questions)
33
+ SESSIONS[session_id] = session
34
+
35
+ return QuizSessionResponse(
36
+ session_id=session_id,
37
+ question_index=0,
38
+ total_questions=len(questions),
39
+ question=questions[0],
40
+ done=False,
41
+ score=session.score,
42
+ )
43
+
44
+
45
+ @app.get("/quiz_session/next_question/{session_id}", response_model=QuizSessionResponse)
46
+ def next_question(session_id: str):
47
+ session = SESSIONS.get(session_id)
48
+ if not session:
49
+ return {"error": "Invalid session"}
50
+
51
+ if session.current_index >= len(session.questions):
52
+ return QuizSessionResponse(
53
+ session_id=session_id,
54
+ question_index=session.current_index,
55
+ total_questions=len(session.questions),
56
+ question=None,
57
+ done=True,
58
+ message="Quiz finished!",
59
+ score=session.score,
60
+ )
61
+
62
+ q = session.questions[session.current_index]
63
+ return QuizSessionResponse(
64
+ session_id=session_id,
65
+ question_index=session.current_index,
66
+ total_questions=len(session.questions),
67
+ question=q,
68
+ done=False,
69
+ score=session.score,
70
+ )
71
+
72
+
73
+ @app.post("/quiz_session/submit_answer/{session_id}", response_model=AnswerResult)
74
+ def submit_answer(session_id: str, answer: AnswerPayload):
75
+ session = SESSIONS.get(session_id)
76
+ if not session:
77
+ return {"error": "Invalid session"}
78
+
79
+ if session.current_index >= len(session.questions):
80
+ return {"error": "No more questions"}
81
+
82
+ q = session.questions[session.current_index]
83
+ is_correct = answer.choiceIndex == q.correctIndex
84
+
85
+ # advance pointer
86
+ session.current_index += 1
87
+
88
+ if is_correct:
89
+ session.score += 1
90
+
91
+ return AnswerResult(
92
+ is_correct=is_correct,
93
+ correctIndex=q.correctIndex,
94
+ explanation=q.explanation,
95
+ next_question_available=session.current_index < len(session.questions),
96
+ score = session.score
97
+ )
98
+
99
+
100
+ SUBJECTS = ["Math", "Science", "History", "English", "Geography"]
101
+
102
+
103
+ # @app.get("/subjects", response_model=SubjectsResponse)
104
+ # async def get_subjects():
105
+ # """
106
+ # Returns the list of available quiz subjects.
107
+ # """
108
+ # return SubjectsResponse(subjects=SUBJECTS)
109
+
110
+ @app.get("/subjects/{grade}")
111
+ def subjects(grade: str):
112
+ result = get_subjects_for_grade(grade)
113
+ return result.model_dump()
api/utils/openai_helper.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Any
3
+ from openai import OpenAI
4
+ import logging
5
+
6
+ logging.basicConfig()
7
+ logger = logging.getLogger(__name__)
8
+ logger.setLevel(logging.INFO)
9
+
10
+
11
+ class OpenAIHelper:
12
+ def __init__(self):
13
+ import os
14
+ from dotenv import load_dotenv
15
+
16
+ load_dotenv()
17
+ self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
18
+
19
+ def ask(self, prompt: str, response_format: Any, structured_output : bool = False):
20
+ logger.info("Fetching question for %s", prompt)
21
+ if structured_output:
22
+ response = self.client.chat.completions.parse(
23
+ model="gpt-4o-mini",
24
+ messages=[{"role": "user", "content": prompt.strip()}],
25
+ response_format=response_format,
26
+ # temperature=0.7
27
+ )
28
+ parsed = response.choices[0].message.parsed
29
+ # logger.info("response = %s", parsed.model_dump_json())
30
+ return parsed
31
+ else:
32
+ response = self.client.chat.completions.create(
33
+ model="gpt-4o-mini",
34
+ messages=[{"role": "user", "content": prompt}],
35
+ # temperature=0.7
36
+ )
37
+ content = response.choices[0].message.content
38
+ logger.info("response = %s", content)
39
+ return content
api/utils/quiz_helper.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from typing import List
3
+
4
+ from api.models import (
5
+ QuizQuestion,
6
+ SubjectListResponse,
7
+ )
8
+ from api.utils.openai_helper import OpenAIHelper
9
+ import logging
10
+
11
+ logging.basicConfig()
12
+ logger = logging.getLogger(__name__)
13
+ logger.setLevel(logging.INFO)
14
+
15
+
16
+ # -------------------------------
17
+ # Fake Question Generator
18
+ # -------------------------------
19
+ def generate_questions_fake(
20
+ subject: str, grade: str, count: int = 3
21
+ ) -> List[QuizQuestion]:
22
+ sample_qs = []
23
+ for i in range(count):
24
+ q_id = str(uuid.uuid4())
25
+ q = QuizQuestion(
26
+ id=q_id,
27
+ question=f"What is {i+1} + {i+2} in {subject} grade {grade}?",
28
+ choices=[str(i + 1), str(i + 2), str(i + 3), str((i + 1) + (i + 2))],
29
+ correctIndex=3,
30
+ explanation=f"{i+1} + {i+2} = {(i+1)+(i+2)}",
31
+ )
32
+ sample_qs.append(q)
33
+ return sample_qs
34
+
35
+
36
+ def generate_questions(subject: str, grade: str, count: int = 3) -> List[QuizQuestion]:
37
+ prompt = (
38
+ f"Generate exactly one unique multiple-choice question for grade {grade} students in {subject}. "
39
+ "The question must be unique, not repeating any topics or calculations from previous questions. "
40
+ )
41
+
42
+ questions: List[QuizQuestion] = []
43
+
44
+ for i in range(count):
45
+ # If not the first iteration, remind the model of what was already generated
46
+ if i == 1:
47
+ prompt += "\nHere are the previous questions you had generated:"
48
+
49
+ q = OpenAIHelper().ask(prompt, QuizQuestion, True)
50
+ q.id = str(uuid.uuid4())
51
+ questions.append(q)
52
+ logger.info("response = %s", q.model_dump_json())
53
+
54
+ # Append the just-generated question to the prompt for context in the next iteration
55
+ prompt += f"\n{i+1}. {q.question}"
56
+
57
+ return questions
58
+
59
+ def get_subjects_for_grade(grade: str) -> SubjectListResponse:
60
+ """
61
+ Uses LLM to generate a list of subjects appropriate for a given grade.
62
+ Returns structured JSON compatible with SubjectListResponse.
63
+ """
64
+ prompt = f"""
65
+ Generate a list of school subjects appropriate for grade {grade} students.
66
+ Return ONLY valid JSON matching this schema:
67
+ {{
68
+ "grade": "{grade}",
69
+ "subjects": ["subject1", "subject2", "subject3"]
70
+ }}
71
+ Example response:
72
+ {{
73
+ "grade": "{grade}",
74
+ "subjects": ["Math", "Science", "English", "History"]
75
+ }}
76
+ """
77
+
78
+ response = OpenAIHelper().ask(prompt, SubjectListResponse, structured_output=True)
79
+ return response
main.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+ from api.server import app
3
+
4
+ if __name__ == "__main__":
5
+ uvicorn.run("api.server:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt ADDED
Binary file (182 Bytes). View file
 
start.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Make sure this file is executable: chmod +x start.sh
4
+
5
+ echo "Starting FastAPI Quiz API..."
6
+
7
+ # Run uvicorn server
8
+ uvicorn api.server:app --host 0.0.0.0 --port 7860 --reload