aki-008 commited on
Commit
66d91dd
·
2 Parent(s): dc2995b 6416677

Merge: from main

Browse files
.gitignore CHANGED
@@ -1,2 +1,228 @@
1
- test.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  test.ipynb
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
217
+
218
+ playground/psql_driver.ipynb
219
+ Backend/Time complexity cheatsheet.pdf
220
+ test/1000-data-science-questions-answers.json
221
+
222
+ test/test.ipynb
223
+ vector_db
224
+ .gitignore
225
+
226
  test.ipynb
227
+ .gitignore
228
+ test.json
Backend/.pre-commit-config.yaml → .pre-commit-config.yaml RENAMED
File without changes
Backend/.gitignore DELETED
@@ -1,218 +0,0 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[codz]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- lib/
18
- lib64/
19
- parts/
20
- sdist/
21
- var/
22
- wheels/
23
- share/python-wheels/
24
- *.egg-info/
25
- .installed.cfg
26
- *.egg
27
- MANIFEST
28
-
29
- # PyInstaller
30
- # Usually these files are written by a python script from a template
31
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
- *.manifest
33
- *.spec
34
-
35
- # Installer logs
36
- pip-log.txt
37
- pip-delete-this-directory.txt
38
-
39
- # Unit test / coverage reports
40
- htmlcov/
41
- .tox/
42
- .nox/
43
- .coverage
44
- .coverage.*
45
- .cache
46
- nosetests.xml
47
- coverage.xml
48
- *.cover
49
- *.py.cover
50
- .hypothesis/
51
- .pytest_cache/
52
- cover/
53
-
54
- # Translations
55
- *.mo
56
- *.pot
57
-
58
- # Django stuff:
59
- *.log
60
- local_settings.py
61
- db.sqlite3
62
- db.sqlite3-journal
63
-
64
- # Flask stuff:
65
- instance/
66
- .webassets-cache
67
-
68
- # Scrapy stuff:
69
- .scrapy
70
-
71
- # Sphinx documentation
72
- docs/_build/
73
-
74
- # PyBuilder
75
- .pybuilder/
76
- target/
77
-
78
- # Jupyter Notebook
79
- .ipynb_checkpoints
80
-
81
- # IPython
82
- profile_default/
83
- ipython_config.py
84
-
85
- # pyenv
86
- # For a library or package, you might want to ignore these files since the code is
87
- # intended to run in multiple environments; otherwise, check them in:
88
- # .python-version
89
-
90
- # pipenv
91
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
- # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
- # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
- # install all needed dependencies.
95
- # Pipfile.lock
96
-
97
- # UV
98
- # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
- # This is especially recommended for binary packages to ensure reproducibility, and is more
100
- # commonly ignored for libraries.
101
- # uv.lock
102
-
103
- # poetry
104
- # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
- # This is especially recommended for binary packages to ensure reproducibility, and is more
106
- # commonly ignored for libraries.
107
- # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
- # poetry.lock
109
- # poetry.toml
110
-
111
- # pdm
112
- # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
- # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
- # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
- # pdm.lock
116
- # pdm.toml
117
- .pdm-python
118
- .pdm-build/
119
-
120
- # pixi
121
- # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
- # pixi.lock
123
- # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
- # in the .venv directory. It is recommended not to include this directory in version control.
125
- .pixi
126
-
127
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
- __pypackages__/
129
-
130
- # Celery stuff
131
- celerybeat-schedule
132
- celerybeat.pid
133
-
134
- # Redis
135
- *.rdb
136
- *.aof
137
- *.pid
138
-
139
- # RabbitMQ
140
- mnesia/
141
- rabbitmq/
142
- rabbitmq-data/
143
-
144
- # ActiveMQ
145
- activemq-data/
146
-
147
- # SageMath parsed files
148
- *.sage.py
149
-
150
- # Environments
151
- .env
152
- .envrc
153
- .venv
154
- env/
155
- venv/
156
- ENV/
157
- env.bak/
158
- venv.bak/
159
-
160
- # Spyder project settings
161
- .spyderproject
162
- .spyproject
163
-
164
- # Rope project settings
165
- .ropeproject
166
-
167
- # mkdocs documentation
168
- /site
169
-
170
- # mypy
171
- .mypy_cache/
172
- .dmypy.json
173
- dmypy.json
174
-
175
- # Pyre type checker
176
- .pyre/
177
-
178
- # pytype static type analyzer
179
- .pytype/
180
-
181
- # Cython debug symbols
182
- cython_debug/
183
-
184
- # PyCharm
185
- # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
- # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
- # and can be added to the global gitignore or merged into this file. For a more nuclear
188
- # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
- # .idea/
190
-
191
- # Abstra
192
- # Abstra is an AI-powered process automation framework.
193
- # Ignore directories containing user credentials, local state, and settings.
194
- # Learn more at https://abstra.io/docs
195
- .abstra/
196
-
197
- # Visual Studio Code
198
- # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
- # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
- # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
- # you could uncomment the following to ignore the entire vscode folder
202
- # .vscode/
203
-
204
- # Ruff stuff:
205
- .ruff_cache/
206
-
207
- # PyPI configuration file
208
- .pypirc
209
-
210
- # Marimo
211
- marimo/_static/
212
- marimo/_lsp/
213
- __marimo__/
214
-
215
- # Streamlit
216
- .streamlit/secrets.toml
217
-
218
- playground/psql_driver.ipynb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Backend/app/api/deps.py CHANGED
@@ -6,6 +6,9 @@ from jose import JWTError, jwt
6
  from app.database import async_session_maker
7
  from app.models import User
8
  from app.config import settings
 
 
 
9
 
10
  security = HTTPBearer()
11
 
@@ -48,3 +51,16 @@ async def get_current_user(
48
 
49
  return user
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from app.database import async_session_maker
7
  from app.models import User
8
  from app.config import settings
9
+ from fastapi import Request
10
+ from chromadb import AsyncHttpClient
11
+ from chromadb.api.models.Collection import Collection
12
 
13
  security = HTTPBearer()
14
 
 
51
 
52
  return user
53
 
54
+
55
+
56
+ async def get_chroma_client(request: Request) -> AsyncHttpClient:
57
+ client = getattr(request.app.state, "chroma_client", None)
58
+ if client is None:
59
+ raise RuntimeError("ChromaDB client is not initialized in App State.")
60
+ return client
61
+
62
+ def get_chroma_collection(request: Request) -> Collection:
63
+ collection = getattr(request.app.state, "chroma_collection", None)
64
+ if collection is None:
65
+ raise RuntimeError("ChromaDB Collection not loaded during application startup.")
66
+ return collection
Backend/app/api/v1/api.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import APIRouter
2
- from app.api.v1.endpoints import auth, students
3
 
4
  api_router = APIRouter()
5
 
@@ -16,3 +16,10 @@ api_router.include_router(
16
  prefix="/students",
17
  tags=["Students"]
18
  )
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter
2
+ from app.api.v1.endpoints import auth, students, quiz
3
 
4
  api_router = APIRouter()
5
 
 
16
  prefix="/students",
17
  tags=["Students"]
18
  )
19
+
20
+ # Include student routes
21
+ api_router.include_router(
22
+ quiz.router,
23
+ prefix="/quiz",
24
+ tags=["quiz"]
25
+ )
Backend/app/api/v1/endpoints/prompts.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_PROMPT = """
2
+ You are an AI question-generation agent.
3
+ Your task is to generate a batch of 10 high-quality MCQ questions strictly based on the following inputs:
4
+
5
+ - {parsed_info}
6
+ - {user_prompt}
7
+ - {retrieved_docs}
8
+
9
+ -----------------------
10
+ GENERATION RULES
11
+ -----------------------
12
+ 1. Generate exactly 10 MCQs.
13
+ 2. Use only information from the provided inputs.
14
+ 3. Each question must be unambiguous, factual, and supported by the given data.
15
+ 4. Each MCQ MUST have exactly four options.
16
+ 5. Only one correct answer is allowed.
17
+ 6. Explanations must be short and directly justify the answer.
18
+ 7. `User_response` must ALWAYS remain an empty string.
19
+ 8. Output MUST be a valid JSON array containing 10 objects.
20
+ 9. Output MUST contain nothing except the JSON array (no commentary or markdown).
21
+
22
+ -----------------------
23
+ REQUIRED JSON FORMAT FOR EACH QUESTION
24
+ -----------------------
25
+ {{
26
+ "question": "Which of the following CLI command can also be used to rename files?",
27
+ "options": [
28
+ "rm",
29
+ "mv",
30
+ "rm -r",
31
+ "none of the mentioned"
32
+ ],
33
+ "answer": "b",
34
+ "explanation": "mv stands for move.",
35
+ "User_response": ""
36
+ }}
37
+
38
+ -----------------------
39
+ ANSWER KEY RULES
40
+ -----------------------
41
+ - 'a' -> options[0]
42
+ - 'b' -> options[1]
43
+ - 'c' -> options[2]
44
+ - 'd' -> options[3]
45
+
46
+ Strictly follow the JSON structure and generate exactly 10 MCQs.
47
+ """
Backend/app/api/v1/endpoints/quiz.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from chromadb import AsyncHttpClient
3
+ from app.models import User
4
+ from app.api.deps import get_db, get_current_user, get_chroma_client
5
+ from app.schema import Quiz_input, QuizOutput
6
+ from .prompts import SYSTEM_PROMPT
7
+ from fastapi import APIRouter, Depends, HTTPException
8
+ from chromadb.api.models.Collection import Collection # Import Collection type
9
+ from app.api.deps import get_chroma_collection
10
+ from app.llm import call_llm
11
+ router = APIRouter(prefix="/quiz")
12
+
13
+ async def search_logic(query: str, collection: Collection):
14
+ results = await collection.query(
15
+ query_texts=[query],
16
+ n_results=5
17
+ )
18
+ return ''.join(results['documents'][0])
19
+
20
+ @router.get("/search_docs")
21
+ async def search_documents(
22
+ query: str,
23
+ collection: Collection = Depends(get_chroma_collection)
24
+ ):
25
+ try:
26
+ return await search_logic(query, collection)
27
+ except Exception as e:
28
+ raise HTTPException(500, f"ChromaDB Query Error: {e}")
29
+
30
+
31
+ @router.post("/", response_model=QuizOutput, status_code=status.HTTP_201_CREATED)
32
+ async def generate_quiz(
33
+ Input_model: Quiz_input,
34
+ collection: Collection = Depends(get_chroma_collection),
35
+ current_user: User = Depends(get_current_user)
36
+ ):
37
+ try:
38
+ query = Input_model.parsed_doc + Input_model.user_prompt
39
+ retrieved_context = await search_logic(query, collection)
40
+
41
+
42
+ if not retrieved_context:
43
+ raise ValueError("No context available to generate quiz.")
44
+ prompt = await prompt_builder(Input_model.parsed_doc, Input_model.user_prompt, retrieved_context)
45
+
46
+ quiz_data_obj = await call_llm(prompt)
47
+
48
+ return quiz_data_obj
49
+
50
+ except Exception as e:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_400_BAD_REQUEST,
53
+ detail=f'Invalid Input: {str(e)}'
54
+ )
55
+
56
+ # #--------Helper Functions--------#
57
+
58
+
59
+ async def prompt_builder(parsed_doc:str, user_prompt:str, docs:str=None):
60
+ prompt = SYSTEM_PROMPT.format(
61
+ parsed_info=parsed_doc,
62
+ user_prompt=user_prompt,
63
+ retrieved_docs=docs
64
+ )
65
+ return prompt
Backend/app/config.py CHANGED
@@ -12,8 +12,14 @@ class Settings(BaseSettings):
12
 
13
  CORS_ORIGINS: list = ["*"]
14
 
 
 
 
 
 
 
15
  class Config:
16
  env_file = ".env"
17
- extra = "ignore" # optional
18
 
19
  settings = Settings()
 
12
 
13
  CORS_ORIGINS: list = ["*"]
14
 
15
+ chroma_host: str
16
+ chroma_port: int
17
+ chroma_collection: str
18
+
19
+ GROQ_API_KEY:str
20
+
21
  class Config:
22
  env_file = ".env"
23
+ extra = "ignore" # quiz
24
 
25
  settings = Settings()
Backend/app/llm.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from pydantic import BaseModel, Field
4
+ from typing import List, Optional, Any
5
+ from app.schema.models import QuizOutput, QuizQuestion
6
+ from app.config import settings
7
+
8
+ client = OpenAI(
9
+ base_url="https://api.groq.com/openai/v1",
10
+ api_key=settings.GROQ_API_KEY
11
+ )
12
+
13
+ async def call_llm(prompt:str):
14
+ try:
15
+ response = client.chat.completions.create(
16
+ # CRUCIAL: Use the LiteLLM format: 'gemini/gemini-2.5-pro'
17
+ model="openai/gpt-oss-20b",
18
+ messages=[
19
+ {"role": "user", "content": prompt}
20
+ ],
21
+ # Use the OpenAI parameter to request JSON output
22
+ response_format={"type": "json_object"},
23
+ temperature=0.7,
24
+ )
25
+
26
+ json_string = response.choices[0].message.content
27
+
28
+ import json
29
+ quiz_data = json.loads(json_string)
30
+ wrapped_data = {"quiz": quiz_data}
31
+ return QuizOutput.model_validate(wrapped_data)
32
+
33
+ except Exception as e:
34
+ print(f"Error calling LiteLLM/Gemini: {e}")
35
+ raise e
Backend/app/main.py CHANGED
@@ -5,7 +5,11 @@ from datetime import datetime
5
  from app.config import settings
6
  from app.database import engine, Base
7
  from app.api.v1.api import api_router
 
 
 
8
 
 
9
 
10
  @asynccontextmanager
11
  async def lifespan(app: FastAPI):
@@ -16,6 +20,21 @@ async def lifespan(app: FastAPI):
16
  async with engine.begin() as conn:
17
  await conn.run_sync(Base.metadata.create_all)
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  print("✅ Tables ready!")
20
  yield
21
  print("🧹 Server shutting down:", datetime.now())
 
5
  from app.config import settings
6
  from app.database import engine, Base
7
  from app.api.v1.api import api_router
8
+ import chromadb
9
+ from chromadb.api.models.Collection import Collection
10
+ from dotenv import load_dotenv
11
 
12
+ load_dotenv()
13
 
14
  @asynccontextmanager
15
  async def lifespan(app: FastAPI):
 
20
  async with engine.begin() as conn:
21
  await conn.run_sync(Base.metadata.create_all)
22
 
23
+ client = await chromadb.AsyncHttpClient(
24
+ host=settings.chroma_host,
25
+ port=settings.chroma_port
26
+ )
27
+ app.state.chroma_client = client
28
+
29
+ try:
30
+ collection: Collection = await client.get_or_create_collection(settings.chroma_collection)
31
+ app.state.chroma_collection = collection
32
+
33
+ count = await collection.count()
34
+ print(f"Successfully loaded collection '{settings.chroma_collection}' with {count} documents.")
35
+ except Exception as e:
36
+ print(f"Failed to load ChromaDB collection: {e}")
37
+
38
  print("✅ Tables ready!")
39
  yield
40
  print("🧹 Server shutting down:", datetime.now())
Backend/app/models/tables.py CHANGED
@@ -19,3 +19,4 @@ class User(Base):
19
  username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
20
  email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
21
  hashed_password: Mapped[str] = mapped_column(String(255))
 
 
19
  username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
20
  email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
21
  hashed_password: Mapped[str] = mapped_column(String(255))
22
+
Backend/app/schema/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from app.schema.models import StudentCreate, StudentUpdate, StudentResponse, UserCreate, Token, LoginRequest
2
 
3
- __all__ = ["StudentCreate", "StudentUpdate", "StudentResponse", "UserCreate", "Token", "LoginRequest"]
 
1
+ from app.schema.models import StudentCreate, StudentUpdate, StudentResponse, UserCreate, Token, LoginRequest, Quiz_input, QuizOutput
2
 
3
+ __all__ = ["StudentCreate", "StudentUpdate", "StudentResponse", "UserCreate", "Token", "LoginRequest", "Quiz_input", "QuizOutput"]
Backend/app/schema/models.py CHANGED
@@ -1,5 +1,5 @@
1
  from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
2
- from typing import Optional
3
  from datetime import datetime
4
 
5
  class StudentBase(BaseModel):
@@ -46,4 +46,19 @@ class Token(BaseModel):
46
  token_type: str
47
 
48
  class LoginResponse(Token):
49
- username: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
2
+ from typing import Optional, Literal, List
3
  from datetime import datetime
4
 
5
  class StudentBase(BaseModel):
 
46
  token_type: str
47
 
48
  class LoginResponse(Token):
49
+ username: str
50
+
51
+ class Quiz_input(BaseModel):
52
+ parsed_doc: str
53
+ user_prompt: str
54
+ # choice: Literal["mcq", "code"]
55
+
56
+ class QuizQuestion(BaseModel):
57
+ question: str
58
+ options: List[str] = Field(..., min_items=2)
59
+ answer: str = Field(..., description="Correct answer key")
60
+ explanation: str
61
+ User_response: str = Field("", alias="User_response")
62
+
63
+ class QuizOutput(BaseModel):
64
+ quiz: List[QuizQuestion] = Field(..., description="A list of 10 generated MCQ questions.")
Backend/app/vector_db.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import chromadb
2
+ from chromadb.config import Settings
3
+
4
+ chroma_client = None
Backend/requirements.txt CHANGED
@@ -8,3 +8,4 @@ passlib[argon2]
8
  python-jose[cryptography]
9
  email-validator
10
 
 
 
8
  python-jose[cryptography]
9
  email-validator
10
 
11
+
RUN.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ npm run dev (frontend)
2
+
3
+ python run.py (Backend)
4
+
5
+ sudo service postgresql start (POstgresql)
6
+
7
+ chroma run --host 0.0.0.0 --port 8080 --path ./chroma_store (chroma_db)
Backend/Readme.md → Readme.md RENAMED
File without changes
test/db CRUD.py DELETED
@@ -1,88 +0,0 @@
1
- import asyncio
2
- from datetime import datetime
3
- from sqlalchemy import String, Integer, DateTime
4
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
5
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
6
- from sqlalchemy import select
7
-
8
- DATABASE_URL = "postgresql+asyncpg://postgres:690869@localhost:5432/studentdb"
9
-
10
- engine = create_async_engine(DATABASE_URL, echo=True)
11
- async_session = async_sessionmaker(engine, expire_on_commit=False)
12
-
13
- class Base(DeclarativeBase):
14
- pass
15
-
16
- class Student(Base):
17
- __tablename__ = "students"
18
-
19
- id: Mapped[int] = mapped_column(primary_key=True)
20
- name: Mapped[str] = mapped_column(String(100))
21
- email: Mapped[str] = mapped_column(String(100))
22
- age: Mapped[int]
23
- grade: Mapped[str] = mapped_column(String(5))
24
- created_at : Mapped[datetime] = mapped_column(default=datetime.utcnow)
25
-
26
- async def create_table():
27
- async with engine.begin() as conn:
28
- await conn.run_sync(Base.metadata.create_all)
29
- print('Tables created successfully !')
30
-
31
- async def add_student(name:str, email: str, age: int, grade: str):
32
- async with async_session() as session:
33
- async with session.begin():
34
- student = Student( name = name, email= email, age=age, grade=grade)
35
- session.add(student)
36
- print(f"added student{name}")
37
-
38
- async def list_students():
39
- async with async_session() as session:
40
- result = await session.execute(select(Student))
41
- students = result.scalars().all()
42
-
43
- for s in students:
44
- print(f" - {s.id}: {s.name}, {s.email}, Grade: {s.grade}, {s.age}")
45
-
46
- async def update_student(student_id: int, new_name: str = None, new_age : int = None, new_grade : int = None ):
47
- async with async_session() as session:
48
- async with session.begin():
49
- student = await session.get(Student, student_id)
50
- if not student:
51
- print(f"❌ Student with id {student_id} not found.")
52
- return
53
- if new_name:
54
- student.name = new_name
55
- if new_grade:
56
- student.grade = new_grade
57
- if new_age:
58
- student.age = new_age
59
-
60
- # await session.commit() ## no need
61
- print(f"✏️ Updated student ID {student_id}")
62
-
63
- async def delete_student(student_id: int):
64
- async with async_session() as session:
65
- async with session.begin():
66
- student = await session.get(Student, student_id)
67
- if not student:
68
- print(f"❌ Student with id {student_id} not found.")
69
- return
70
-
71
- await session.delete(student)
72
- # await session.commit() ## no need
73
- print(f"🗑️ Deleted student ID {student_id}")
74
-
75
-
76
- async def main():
77
- await create_table()
78
- await add_student("Akshat Mehta", "akshat@example.com", 21, "A+")
79
- await add_student("Nikhil Sharma", "nikhil@example.com", 22, "B")
80
- await list_students()
81
-
82
- await update_student(1, new_age=52)
83
- await delete_student(2)
84
- print('-'*100)
85
- await list_students()
86
-
87
- if __name__ == "__main__":
88
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/db_fastapi_CRUD.py DELETED
@@ -1,127 +0,0 @@
1
- import asyncio
2
- from typing import List, Optional
3
- from contextlib import asynccontextmanager
4
- from datetime import datetime
5
- from fastapi import FastAPI, HTTPException, status
6
- from sqlalchemy import String, Integer, DateTime, select
7
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
8
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
9
- from pydantic import BaseModel, ConfigDict
10
-
11
-
12
- DATABASE_URL = "postgresql+asyncpg://postgres:690869@localhost:5432/studentdb"
13
-
14
- engine = create_async_engine(DATABASE_URL, echo=True)
15
- async_session = async_sessionmaker(engine, expire_on_commit=False)
16
-
17
- class Base(DeclarativeBase):
18
- pass
19
-
20
- class Student(Base):
21
- __tablename__ = "students"
22
-
23
- id: Mapped[int] = mapped_column(primary_key=True)
24
- name: Mapped[str] = mapped_column(String(100))
25
- email: Mapped[str] = mapped_column(String(100))
26
- age: Mapped[int]
27
- grade: Mapped[str] = mapped_column(String(5))
28
- created_at : Mapped[datetime] = mapped_column(default=datetime.utcnow)
29
-
30
- class StudentCreate(BaseModel):
31
- name: str
32
- email: str
33
- age: int
34
- grade: str
35
-
36
- class StudentUpdate(BaseModel):
37
- name: Optional[str] = None
38
- email: Optional[str] = None
39
- age: Optional[int] = None
40
- grade: Optional[str] = None
41
-
42
- class StudentOut(BaseModel):
43
- id: int
44
- name: str
45
- email: str
46
- age: int
47
- grade: str
48
- created_at: datetime
49
-
50
- model_config = ConfigDict(from_attributes=True)
51
-
52
-
53
-
54
- @asynccontextmanager
55
- async def lifespan(app: FastAPI):
56
- async with engine.begin() as conn:
57
- await conn.run_sync(Base.metadata.create_all)
58
- print("Tables created !!!!")
59
- yield
60
- await engine.dispose()
61
- print("🔻 Database connection closed.")
62
-
63
- app = FastAPI(title="Async student CRUD API", lifespan=lifespan)
64
-
65
- @app.post("/students/", response_model=StudentOut, status_code=status.HTTP_201_CREATED)
66
- async def create_student(student: StudentCreate):
67
- async with async_session() as session:
68
- async with session.begin():
69
- student = Student(**student.model_dump())
70
- session.add(student)
71
- await session.refresh(student)
72
- return student
73
-
74
- @app.get("/students/", response_model=List[StudentOut])
75
- async def list_students():
76
- async with async_session() as session:
77
- result = await session.execute(select(Student))
78
- students = result.scalars().all()
79
- return students
80
-
81
- @app.get("/students/{id}", response_model=StudentOut)
82
- async def get_student(id: int):
83
- async with async_session() as session:
84
- student = await session.get(Student, id)
85
- if not student:
86
- raise HTTPException(status_code=404, detail="Student not found")
87
- return student
88
-
89
- @app.put("/students/{id}/", response_model=StudentOut)
90
- async def update_student(id: int, update_data: StudentUpdate):
91
- async with async_session() as session:
92
- async with session.begin():
93
- student = await session.get(Student, id)
94
- if not student:
95
- raise HTTPException(status_code=404, detail="student not found")
96
-
97
- update_dict = update_data.model_dump(exclude_unset=True)
98
- for key , value in update_dict.items():
99
- setattr(student, key , value)
100
-
101
- await session.refresh(student)
102
- return student
103
-
104
- @app.delete("/students/{id}", status_code=status.HTTP_204_NO_CONTENT)
105
- async def delete_student(id: int):
106
- async with async_session() as session:
107
- async with session.begin():
108
- student = await session.get(Student, id)
109
- if not student:
110
- raise HTTPException(status_code=404, detail="student not found")
111
- await session.delete(student)
112
- return None
113
-
114
-
115
- # async def main():
116
- # # await create_table()
117
- # await add_student("Akshat Mehta", "akshat@example.com", 21, "A+")
118
- # await add_student("Nikhil Sharma", "nikhil@example.com", 22, "B")
119
- # await list_students()
120
-
121
- # await update_student(1, new_age=52)
122
- # await delete_student(2)
123
- # print('-'*100)
124
- # await list_students()
125
-
126
- # if __name__ == "__main__":
127
- # asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/main.py DELETED
@@ -1,445 +0,0 @@
1
- """
2
- FastAPI + PostgreSQL Student Management System
3
- Complete async implementation with SQLAlchemy, authentication, and validation
4
-
5
- Requirements:
6
- pip install fastapi uvicorn sqlalchemy asyncpg psycopg2-binary python-jose[cryptography] passlib[bcrypt] python-multipart
7
- """
8
-
9
- from fastapi import FastAPI, HTTPException, Depends, status
10
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
13
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
14
- from sqlalchemy import select, String
15
- from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
16
- from typing import Optional, List
17
- from datetime import datetime, timedelta
18
- from jose import JWTError, jwt
19
- from passlib.context import CryptContext
20
- import os
21
- from contextlib import asynccontextmanager
22
- # ======================== CONFIGURATION ========================
23
- DATABASE_URL = os.getenv(
24
- "DATABASE_URL",
25
- "postgresql+asyncpg://postgres:690869@172.26.157.164:5432/studentdb"
26
- )
27
- SECRET_KEY = os.getenv("SECRET_KEY", "production")
28
- ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
30
-
31
- # ======================== DATABASE SETUP ========================
32
- engine = create_async_engine(DATABASE_URL, echo=True)
33
- async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
34
-
35
- class Base(DeclarativeBase):
36
- pass
37
-
38
- class Student(Base):
39
- __tablename__ = "students"
40
-
41
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
42
- name: Mapped[str] = mapped_column(String(100))
43
- email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
44
- age: Mapped[int]
45
- grade: Mapped[str] = mapped_column(String(5))
46
- created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
47
-
48
- class User(Base):
49
- __tablename__ = "users"
50
-
51
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
52
- username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
53
- hashed_password: Mapped[str] = mapped_column(String(255))
54
-
55
- # ======================== PYDANTIC MODELS ========================
56
- class StudentBase(BaseModel):
57
- name: str = Field(..., min_length=2, max_length=100, description="Student full name")
58
- email: EmailStr = Field(..., description="Student email address")
59
- age: int = Field(..., ge=5, le=100, description="Student age (5-100)")
60
- grade: str = Field(..., pattern="^[A-F][+-]?$", description="Grade (A-F with optional + or -)")
61
-
62
- @field_validator('name')
63
- def validate_name(cls, v):
64
- if not v.strip():
65
- raise ValueError('Name cannot be empty or just whitespace')
66
- return v.strip()
67
-
68
- @field_validator('grade')
69
- def validate_grade(cls, v):
70
- v = v.upper()
71
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
72
- raise ValueError('Invalid grade format')
73
- return v
74
-
75
- class StudentCreate(StudentBase):
76
- pass
77
-
78
- class StudentUpdate(BaseModel):
79
- name: Optional[str] = Field(None, min_length=2, max_length=100)
80
- email: Optional[EmailStr] = None
81
- age: Optional[int] = Field(None, ge=5, le=100)
82
- grade: Optional[str] = Field(None, pattern="^[A-F][+-]?$")
83
-
84
- @field_validator('grade')
85
- def validate_grade(cls, v):
86
- if v is not None:
87
- v = v.upper()
88
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
89
- raise ValueError('Invalid grade format')
90
- return v
91
-
92
- class StudentResponse(StudentBase):
93
- id: int
94
- created_at: datetime
95
-
96
- model_config = ConfigDict(from_attributes=True)
97
-
98
- class UserCreate(BaseModel):
99
- username: str = Field(..., min_length=3, max_length=50)
100
- password: str = Field(..., min_length=6, max_length=72)
101
-
102
- @field_validator('password')
103
- def validate_password(cls, v):
104
- if len(v.encode('utf-8')) > 72:
105
- raise ValueError('Password cannot exceed 72 bytes')
106
- return v
107
-
108
- class Token(BaseModel):
109
- access_token: str
110
- token_type: str
111
-
112
- # ======================== SECURITY ========================
113
- pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
114
- security = HTTPBearer()
115
-
116
- def verify_password(plain_password: str, hashed_password: str) -> bool:
117
- # Truncate password to 72 bytes for bcrypt compatibility
118
- password_bytes = plain_password.encode('utf-8')[:72]
119
- plain_password_truncated = password_bytes.decode('utf-8', errors='ignore')
120
- return pwd_context.verify(plain_password_truncated, hashed_password)
121
-
122
- def get_password_hash(password: str) -> str:
123
- # Truncate password to 72 bytes for bcrypt compatibility
124
- password_bytes = password.encode('utf-8')[:72]
125
- password_truncated = password_bytes.decode('utf-8', errors='ignore')
126
- return pwd_context.hash(password_truncated)
127
-
128
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
129
- to_encode = data.copy()
130
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
131
- to_encode.update({"exp": expire})
132
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
133
- return encoded_jwt
134
-
135
- async def get_current_user(
136
- credentials: HTTPAuthorizationCredentials = Depends(security),
137
- db: AsyncSession = Depends(lambda: async_session_maker())
138
- ):
139
- credentials_exception = HTTPException(
140
- status_code=status.HTTP_401_UNAUTHORIZED,
141
- detail="Could not validate credentials",
142
- headers={"WWW-Authenticate": "Bearer"},
143
- )
144
- try:
145
- token = credentials.credentials
146
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
147
- username: str = payload.get("sub")
148
- if username is None:
149
- raise credentials_exception
150
- except JWTError:
151
- raise credentials_exception
152
-
153
- result = await db.execute(select(User).filter(User.username == username))
154
- user = result.scalar_one_or_none()
155
- if user is None:
156
- raise credentials_exception
157
- return user
158
-
159
- # ======================== DATABASE DEPENDENCY ========================
160
- async def get_db():
161
- async with async_session_maker() as session:
162
- try:
163
- yield session
164
- await session.commit()
165
- except Exception:
166
- await session.rollback()
167
- raise
168
- finally:
169
- await session.close()
170
-
171
- # new on event alternative
172
-
173
- @asynccontextmanager
174
- async def lifespan(app: FastAPI):
175
- print("🏗️ Server starting:", datetime.now())
176
- print("🔧 Creating tables if they don't exist...")
177
-
178
- async with engine.begin() as conn:
179
- await conn.run_sync(Base.metadata.create_all) # Create tables
180
-
181
- print("✅ Tables ready!")
182
- yield
183
- print("🧹 Server shutting down:", datetime.now())
184
-
185
- # ======================== FASTAPI APP ========================
186
- app = FastAPI(
187
- title="Student Management API",
188
- description="FastAPI + PostgreSQL with SQLAlchemy async",
189
- version="1.0.0",
190
- lifespan=lifespan
191
-
192
- )
193
-
194
- # CORS Configuration
195
- app.add_middleware(
196
- CORSMiddleware,
197
- allow_origins=["*"], # In production, specify allowed origins
198
- allow_credentials=True,
199
- allow_methods=["*"],
200
- allow_headers=["*"],
201
- )
202
-
203
-
204
- # ======================== AUTHENTICATION ROUTES ========================
205
- @app.post("/auth/register", response_model=dict, tags=["Authentication"])
206
- async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
207
- """Register a new user"""
208
- try:
209
- result = await db.execute(select(User).filter(User.username == user.username))
210
- existing_user = result.scalar_one_or_none()
211
-
212
- if existing_user:
213
- raise HTTPException(
214
- status_code=status.HTTP_400_BAD_REQUEST,
215
- detail="Username already registered"
216
- )
217
-
218
- new_user = User(
219
- username=user.username,
220
- hashed_password=get_password_hash(user.password)
221
- )
222
- db.add(new_user)
223
- await db.commit()
224
-
225
- return {"message": "User registered successfully", "username": user.username}
226
- except HTTPException:
227
- raise
228
- except Exception as e:
229
- raise HTTPException(
230
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
231
- detail=f"Registration failed: {str(e)}"
232
- )
233
-
234
- @app.post("/auth/login", response_model=Token, tags=["Authentication"])
235
- async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
236
- """Login and get access token"""
237
- try:
238
- result = await db.execute(select(User).filter(User.username == username))
239
- user = result.scalar_one_or_none()
240
-
241
- if not user or not verify_password(password, user.hashed_password):
242
- raise HTTPException(
243
- status_code=status.HTTP_401_UNAUTHORIZED,
244
- detail="Incorrect username or password",
245
- headers={"WWW-Authenticate": "Bearer"},
246
- )
247
-
248
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
249
- access_token = create_access_token(
250
- data={"sub": user.username},
251
- expires_delta=access_token_expires
252
- )
253
-
254
- return {"access_token": access_token, "token_type": "bearer"}
255
- except HTTPException:
256
- raise
257
- except Exception as e:
258
- raise HTTPException(
259
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
260
- detail=f"Login failed: {str(e)}"
261
- )
262
-
263
- # ======================== STUDENT CRUD ROUTES ========================
264
- @app.post("/students/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED, tags=["Students"])
265
- async def create_student(
266
- student: StudentCreate,
267
- db: AsyncSession = Depends(get_db),
268
- current_user: User = Depends(get_current_user)
269
- ):
270
- """Create a new student (Protected)"""
271
- try:
272
- # Check if email already exists
273
- result = await db.execute(select(Student).filter(Student.email == student.email))
274
- existing_student = result.scalar_one_or_none()
275
-
276
- if existing_student:
277
- raise HTTPException(
278
- status_code=status.HTTP_400_BAD_REQUEST,
279
- detail=f"Student with email {student.email} already exists"
280
- )
281
-
282
- new_student = Student(**student.model_dump())
283
- db.add(new_student)
284
- await db.commit()
285
- await db.refresh(new_student)
286
-
287
- return new_student
288
- except HTTPException:
289
- raise
290
- except Exception as e:
291
- raise HTTPException(
292
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
293
- detail=f"Failed to create student: {str(e)}"
294
- )
295
-
296
- @app.get("/students/", response_model=List[StudentResponse], tags=["Students"])
297
- async def get_all_students(
298
- skip: int = 0,
299
- limit: int = 100,
300
- db: AsyncSession = Depends(get_db),
301
- current_user: User = Depends(get_current_user)
302
- ):
303
- """Get all students with pagination (Protected)"""
304
- try:
305
- result = await db.execute(
306
- select(Student).offset(skip).limit(limit)
307
- )
308
- students = result.scalars().all()
309
- return students
310
- except Exception as e:
311
- raise HTTPException(
312
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
313
- detail=f"Failed to fetch students: {str(e)}"
314
- )
315
-
316
- @app.get("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
317
- async def get_student(
318
- student_id: int,
319
- db: AsyncSession = Depends(get_db),
320
- current_user: User = Depends(get_current_user)
321
- ):
322
- """Get a specific student by ID (Protected)"""
323
- try:
324
- result = await db.execute(select(Student).filter(Student.id == student_id))
325
- student = result.scalar_one_or_none()
326
-
327
- if not student:
328
- raise HTTPException(
329
- status_code=status.HTTP_404_NOT_FOUND,
330
- detail=f"Student with ID {student_id} not found"
331
- )
332
-
333
- return student
334
- except HTTPException:
335
- raise
336
- except Exception as e:
337
- raise HTTPException(
338
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
339
- detail=f"Failed to fetch student: {str(e)}"
340
- )
341
-
342
- @app.put("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
343
- async def update_student(
344
- student_id: int,
345
- student_update: StudentUpdate,
346
- db: AsyncSession = Depends(get_db),
347
- current_user: User = Depends(get_current_user)
348
- ):
349
- """Update a student's information (Protected)"""
350
- try:
351
- result = await db.execute(select(Student).filter(Student.id == student_id))
352
- student = result.scalar_one_or_none()
353
-
354
- if not student:
355
- raise HTTPException(
356
- status_code=status.HTTP_404_NOT_FOUND,
357
- detail=f"Student with ID {student_id} not found"
358
- )
359
-
360
- # Update only provided fields
361
- update_data = student_update.model_dump(exclude_unset=True)
362
-
363
- # Check email uniqueness if email is being updated
364
- if "email" in update_data:
365
- result = await db.execute(
366
- select(Student).filter(
367
- Student.email == update_data["email"],
368
- Student.id != student_id
369
- )
370
- )
371
- existing = result.scalar_one_or_none()
372
- if existing:
373
- raise HTTPException(
374
- status_code=status.HTTP_400_BAD_REQUEST,
375
- detail=f"Email {update_data['email']} is already in use"
376
- )
377
-
378
- for key, value in update_data.items():
379
- setattr(student, key, value)
380
-
381
- await db.commit()
382
- await db.refresh(student)
383
-
384
- return student
385
- except HTTPException:
386
- raise
387
- except Exception as e:
388
- raise HTTPException(
389
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
- detail=f"Failed to update student: {str(e)}"
391
- )
392
-
393
- @app.patch("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
394
- async def partial_update_student(
395
- student_id: int,
396
- student_update: StudentUpdate,
397
- db: AsyncSession = Depends(get_db),
398
- current_user: User = Depends(get_current_user)
399
- ):
400
- """Partially update a student (same as PUT for this implementation) (Protected)"""
401
- return await update_student(student_id, student_update, db, current_user)
402
-
403
- @app.delete("/students/{student_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Students"])
404
- async def delete_student(
405
- student_id: int,
406
- db: AsyncSession = Depends(get_db),
407
- current_user: User = Depends(get_current_user)
408
- ):
409
- """Delete a student (Protected)"""
410
- try:
411
- result = await db.execute(select(Student).filter(Student.id == student_id))
412
- student = result.scalar_one_or_none()
413
-
414
- if not student:
415
- raise HTTPException(
416
- status_code=status.HTTP_404_NOT_FOUND,
417
- detail=f"Student with ID {student_id} not found"
418
- )
419
-
420
- await db.delete(student)
421
- await db.commit()
422
-
423
- return None
424
- except HTTPException:
425
- raise
426
- except Exception as e:
427
- raise HTTPException(
428
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
- detail=f"Failed to delete student: {str(e)}"
430
- )
431
-
432
- # ======================== HEALTH CHECK ========================
433
- @app.get("/", tags=["Health"])
434
- async def root():
435
- """Health check endpoint"""
436
- return {
437
- "status": "healthy",
438
- "message": "Student Management API is running",
439
- "version": "1.0.0"
440
- }
441
-
442
- # ======================== RUN APPLICATION ========================
443
- if __name__ == "__main__":
444
- import uvicorn
445
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/test.ipynb DELETED
@@ -1,78 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "code",
5
- "execution_count": 1,
6
- "id": "d0e4e192",
7
- "metadata": {},
8
- "outputs": [],
9
- "source": [
10
- "import asyncio\n",
11
- "from datetime import datetime\n",
12
- "from sqlalchemy import String, Integer, DateTime\n",
13
- "from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n",
14
- "from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n",
15
- "from sqlalchemy import select\n",
16
- "\n",
17
- "DATABASE_URL = \"postgresql+asyncpg://postgres:690869@localhost:5432/studentdb\"\n",
18
- "\n",
19
- "engine = create_async_engine(DATABASE_URL, echo=True)\n",
20
- "async_session = async_sessionmaker(engine, expire_on_commit=False)\n",
21
- "\n",
22
- "# class Base(DeclarativeBase):\n",
23
- "# pass\n",
24
- "Base = DeclarativeBase()"
25
- ]
26
- },
27
- {
28
- "cell_type": "code",
29
- "execution_count": 4,
30
- "id": "f2813d66",
31
- "metadata": {},
32
- "outputs": [
33
- {
34
- "data": {
35
- "text/plain": [
36
- "<sqlalchemy.orm.decl_api.DeclarativeBase at 0x7df42272b620>"
37
- ]
38
- },
39
- "execution_count": 4,
40
- "metadata": {},
41
- "output_type": "execute_result"
42
- }
43
- ],
44
- "source": [
45
- "Base\n"
46
- ]
47
- },
48
- {
49
- "cell_type": "code",
50
- "execution_count": null,
51
- "id": "52154570",
52
- "metadata": {},
53
- "outputs": [],
54
- "source": []
55
- }
56
- ],
57
- "metadata": {
58
- "kernelspec": {
59
- "display_name": "prep",
60
- "language": "python",
61
- "name": "python3"
62
- },
63
- "language_info": {
64
- "codemirror_mode": {
65
- "name": "ipython",
66
- "version": 3
67
- },
68
- "file_extension": ".py",
69
- "mimetype": "text/x-python",
70
- "name": "python",
71
- "nbconvert_exporter": "python",
72
- "pygments_lexer": "ipython3",
73
- "version": "3.13.9"
74
- }
75
- },
76
- "nbformat": 4,
77
- "nbformat_minor": 5
78
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/test.py DELETED
@@ -1,445 +0,0 @@
1
- """
2
- FastAPI + PostgreSQL Student Management System
3
- Complete async implementation with SQLAlchemy, authentication, and validation
4
-
5
- Requirements:
6
- pip install fastapi uvicorn sqlalchemy asyncpg psycopg2-binary python-jose[cryptography] passlib[bcrypt] python-multipart
7
- """
8
-
9
- from fastapi import FastAPI, HTTPException, Depends, status
10
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
13
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
14
- from sqlalchemy import select, String
15
- from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
16
- from typing import Optional, List
17
- from datetime import datetime, timedelta
18
- from jose import JWTError, jwt
19
- from passlib.context import CryptContext
20
- import os
21
- from contextlib import asynccontextmanager
22
- # ======================== CONFIGURATION ========================
23
- DATABASE_URL = os.getenv(
24
- "DATABASE_URL",
25
- "postgresql+asyncpg://postgres:690869@172.26.157.164:5432/studentdb"
26
- )
27
- SECRET_KEY = os.getenv("SECRET_KEY", "production")
28
- ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
30
-
31
- # ======================== DATABASE SETUP ========================
32
- engine = create_async_engine(DATABASE_URL, echo=True)
33
- async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
34
-
35
- class Base(DeclarativeBase):
36
- pass
37
-
38
- class Student(Base):
39
- __tablename__ = "students"
40
-
41
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
42
- name: Mapped[str] = mapped_column(String(100))
43
- email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
44
- age: Mapped[int]
45
- grade: Mapped[str] = mapped_column(String(5))
46
- created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
47
-
48
- class User(Base):
49
- __tablename__ = "users"
50
-
51
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
52
- username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
53
- hashed_password: Mapped[str] = mapped_column(String(255))
54
-
55
- # ======================== PYDANTIC MODELS ========================
56
- class StudentBase(BaseModel):
57
- name: str = Field(..., min_length=2, max_length=100, description="Student full name")
58
- email: EmailStr = Field(..., description="Student email address")
59
- age: int = Field(..., ge=5, le=100, description="Student age (5-100)")
60
- grade: str = Field(..., pattern="^[A-F][+-]?$", description="Grade (A-F with optional + or -)")
61
-
62
- @field_validator('name')
63
- def validate_name(cls, v):
64
- if not v.strip():
65
- raise ValueError('Name cannot be empty or just whitespace')
66
- return v.strip()
67
-
68
- @field_validator('grade')
69
- def validate_grade(cls, v):
70
- v = v.upper()
71
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
72
- raise ValueError('Invalid grade format')
73
- return v
74
-
75
- class StudentCreate(StudentBase):
76
- pass
77
-
78
- class StudentUpdate(BaseModel):
79
- name: Optional[str] = Field(None, min_length=2, max_length=100)
80
- email: Optional[EmailStr] = None
81
- age: Optional[int] = Field(None, ge=5, le=100)
82
- grade: Optional[str] = Field(None, pattern="^[A-F][+-]?$")
83
-
84
- @field_validator('grade')
85
- def validate_grade(cls, v):
86
- if v is not None:
87
- v = v.upper()
88
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
89
- raise ValueError('Invalid grade format')
90
- return v
91
-
92
- class StudentResponse(StudentBase):
93
- id: int
94
- created_at: datetime
95
-
96
- model_config = ConfigDict(from_attributes=True)
97
-
98
- class UserCreate(BaseModel):
99
- username: str = Field(..., min_length=3, max_length=50)
100
- password: str = Field(..., min_length=6, max_length=72)
101
-
102
- @field_validator('password')
103
- def validate_password(cls, v):
104
- if len(v.encode('utf-8')) > 72:
105
- raise ValueError('Password cannot exceed 72 bytes')
106
- return v
107
-
108
- class Token(BaseModel):
109
- access_token: str
110
- token_type: str
111
-
112
- # ======================== SECURITY ========================
113
- pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
114
- security = HTTPBearer()
115
-
116
- def verify_password(plain_password: str, hashed_password: str) -> bool:
117
- # Truncate password to 72 bytes for bcrypt compatibility
118
- password_bytes = plain_password.encode('utf-8')[:72]
119
- plain_password_truncated = password_bytes.decode('utf-8', errors='ignore')
120
- return pwd_context.verify(plain_password_truncated, hashed_password)
121
-
122
- def get_password_hash(password: str) -> str:
123
- # Truncate password to 72 bytes for bcrypt compatibility
124
- password_bytes = password.encode('utf-8')[:72]
125
- password_truncated = password_bytes.decode('utf-8', errors='ignore')
126
- return pwd_context.hash(password_truncated)
127
-
128
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
129
- to_encode = data.copy()
130
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
131
- to_encode.update({"exp": expire})
132
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
133
- return encoded_jwt
134
-
135
- async def get_current_user(
136
- credentials: HTTPAuthorizationCredentials = Depends(security),
137
- db: AsyncSession = Depends(lambda: async_session_maker())
138
- ):
139
- credentials_exception = HTTPException(
140
- status_code=status.HTTP_401_UNAUTHORIZED,
141
- detail="Could not validate credentials",
142
- headers={"WWW-Authenticate": "Bearer"},
143
- )
144
- try:
145
- token = credentials.credentials
146
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
147
- username: str = payload.get("sub")
148
- if username is None:
149
- raise credentials_exception
150
- except JWTError:
151
- raise credentials_exception
152
-
153
- result = await db.execute(select(User).filter(User.username == username))
154
- user = result.scalar_one_or_none()
155
- if user is None:
156
- raise credentials_exception
157
- return user
158
-
159
- # ======================== DATABASE DEPENDENCY ========================
160
- async def get_db():
161
- async with async_session_maker() as session:
162
- try:
163
- yield session
164
- await session.commit()
165
- except Exception:
166
- await session.rollback()
167
- raise
168
- finally:
169
- await session.close()
170
-
171
- # new on event alternative
172
-
173
- @asynccontextmanager
174
- async def lifespan(app: FastAPI):
175
- print("🏗️ Server starting:", datetime.now())
176
- print("🔧 Creating tables if they don't exist...")
177
-
178
- async with engine.begin() as conn:
179
- await conn.run_sync(Base.metadata.create_all) # Create tables
180
-
181
- print("✅ Tables ready!")
182
- yield
183
- print("🧹 Server shutting down:", datetime.now())
184
-
185
- # ======================== FASTAPI APP ========================
186
- app = FastAPI(
187
- title="Student Management API",
188
- description="FastAPI + PostgreSQL with SQLAlchemy async",
189
- version="1.0.0",
190
- lifespan=lifespan
191
-
192
- )
193
-
194
- # CORS Configuration
195
- app.add_middleware(
196
- CORSMiddleware,
197
- allow_origins=["*"], # In production, specify allowed origins
198
- allow_credentials=True,
199
- allow_methods=["*"],
200
- allow_headers=["*"],
201
- )
202
-
203
-
204
- # ======================== AUTHENTICATION ROUTES ========================
205
- @app.post("/auth/register", response_model=dict, tags=["Authentication"])
206
- async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
207
- """Register a new user"""
208
- try:
209
- result = await db.execute(select(User).filter(User.username == user.username))
210
- existing_user = result.scalar_one_or_none()
211
-
212
- if existing_user:
213
- raise HTTPException(
214
- status_code=status.HTTP_400_BAD_REQUEST,
215
- detail="Username already registered"
216
- )
217
-
218
- new_user = User(
219
- username=user.username,
220
- hashed_password=get_password_hash(user.password)
221
- )
222
- db.add(new_user)
223
- await db.commit()
224
-
225
- return {"message": "User registered successfully", "username": user.username}
226
- except HTTPException:
227
- raise
228
- except Exception as e:
229
- raise HTTPException(
230
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
231
- detail=f"Registration failed: {str(e)}"
232
- )
233
-
234
- @app.post("/auth/login", response_model=Token, tags=["Authentication"])
235
- async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
236
- """Login and get access token"""
237
- try:
238
- result = await db.execute(select(User).filter(User.username == username))
239
- user = result.scalar_one_or_none()
240
-
241
- if not user or not verify_password(password, user.hashed_password):
242
- raise HTTPException(
243
- status_code=status.HTTP_401_UNAUTHORIZED,
244
- detail="Incorrect username or password",
245
- headers={"WWW-Authenticate": "Bearer"},
246
- )
247
-
248
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
249
- access_token = create_access_token(
250
- data={"sub": user.username},
251
- expires_delta=access_token_expires
252
- )
253
-
254
- return {"access_token": access_token, "token_type": "bearer"}
255
- except HTTPException:
256
- raise
257
- except Exception as e:
258
- raise HTTPException(
259
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
260
- detail=f"Login failed: {str(e)}"
261
- )
262
-
263
- # ======================== STUDENT CRUD ROUTES ========================
264
- @app.post("/students/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED, tags=["Students"])
265
- async def create_student(
266
- student: StudentCreate,
267
- db: AsyncSession = Depends(get_db),
268
- current_user: User = Depends(get_current_user)
269
- ):
270
- """Create a new student (Protected)"""
271
- try:
272
- # Check if email already exists
273
- result = await db.execute(select(Student).filter(Student.email == student.email))
274
- existing_student = result.scalar_one_or_none()
275
-
276
- if existing_student:
277
- raise HTTPException(
278
- status_code=status.HTTP_400_BAD_REQUEST,
279
- detail=f"Student with email {student.email} already exists"
280
- )
281
-
282
- new_student = Student(**student.model_dump())
283
- db.add(new_student)
284
- await db.commit()
285
- await db.refresh(new_student)
286
-
287
- return new_student
288
- except HTTPException:
289
- raise
290
- except Exception as e:
291
- raise HTTPException(
292
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
293
- detail=f"Failed to create student: {str(e)}"
294
- )
295
-
296
- @app.get("/students/", response_model=List[StudentResponse], tags=["Students"])
297
- async def get_all_students(
298
- skip: int = 0,
299
- limit: int = 100,
300
- db: AsyncSession = Depends(get_db),
301
- current_user: User = Depends(get_current_user)
302
- ):
303
- """Get all students with pagination (Protected)"""
304
- try:
305
- result = await db.execute(
306
- select(Student).offset(skip).limit(limit)
307
- )
308
- students = result.scalars().all()
309
- return students
310
- except Exception as e:
311
- raise HTTPException(
312
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
313
- detail=f"Failed to fetch students: {str(e)}"
314
- )
315
-
316
- @app.get("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
317
- async def get_student(
318
- student_id: int,
319
- db: AsyncSession = Depends(get_db),
320
- current_user: User = Depends(get_current_user)
321
- ):
322
- """Get a specific student by ID (Protected)"""
323
- try:
324
- result = await db.execute(select(Student).filter(Student.id == student_id))
325
- student = result.scalar_one_or_none()
326
-
327
- if not student:
328
- raise HTTPException(
329
- status_code=status.HTTP_404_NOT_FOUND,
330
- detail=f"Student with ID {student_id} not found"
331
- )
332
-
333
- return student
334
- except HTTPException:
335
- raise
336
- except Exception as e:
337
- raise HTTPException(
338
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
339
- detail=f"Failed to fetch student: {str(e)}"
340
- )
341
-
342
- @app.put("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
343
- async def update_student(
344
- student_id: int,
345
- student_update: StudentUpdate,
346
- db: AsyncSession = Depends(get_db),
347
- current_user: User = Depends(get_current_user)
348
- ):
349
- """Update a student's information (Protected)"""
350
- try:
351
- result = await db.execute(select(Student).filter(Student.id == student_id))
352
- student = result.scalar_one_or_none()
353
-
354
- if not student:
355
- raise HTTPException(
356
- status_code=status.HTTP_404_NOT_FOUND,
357
- detail=f"Student with ID {student_id} not found"
358
- )
359
-
360
- # Update only provided fields
361
- update_data = student_update.model_dump(exclude_unset=True)
362
-
363
- # Check email uniqueness if email is being updated
364
- if "email" in update_data:
365
- result = await db.execute(
366
- select(Student).filter(
367
- Student.email == update_data["email"],
368
- Student.id != student_id
369
- )
370
- )
371
- existing = result.scalar_one_or_none()
372
- if existing:
373
- raise HTTPException(
374
- status_code=status.HTTP_400_BAD_REQUEST,
375
- detail=f"Email {update_data['email']} is already in use"
376
- )
377
-
378
- for key, value in update_data.items():
379
- setattr(student, key, value)
380
-
381
- await db.commit()
382
- await db.refresh(student)
383
-
384
- return student
385
- except HTTPException:
386
- raise
387
- except Exception as e:
388
- raise HTTPException(
389
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
- detail=f"Failed to update student: {str(e)}"
391
- )
392
-
393
- @app.patch("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
394
- async def partial_update_student(
395
- student_id: int,
396
- student_update: StudentUpdate,
397
- db: AsyncSession = Depends(get_db),
398
- current_user: User = Depends(get_current_user)
399
- ):
400
- """Partially update a student (same as PUT for this implementation) (Protected)"""
401
- return await update_student(student_id, student_update, db, current_user)
402
-
403
- @app.delete("/students/{student_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Students"])
404
- async def delete_student(
405
- student_id: int,
406
- db: AsyncSession = Depends(get_db),
407
- current_user: User = Depends(get_current_user)
408
- ):
409
- """Delete a student (Protected)"""
410
- try:
411
- result = await db.execute(select(Student).filter(Student.id == student_id))
412
- student = result.scalar_one_or_none()
413
-
414
- if not student:
415
- raise HTTPException(
416
- status_code=status.HTTP_404_NOT_FOUND,
417
- detail=f"Student with ID {student_id} not found"
418
- )
419
-
420
- await db.delete(student)
421
- await db.commit()
422
-
423
- return None
424
- except HTTPException:
425
- raise
426
- except Exception as e:
427
- raise HTTPException(
428
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
- detail=f"Failed to delete student: {str(e)}"
430
- )
431
-
432
- # ======================== HEALTH CHECK ========================
433
- @app.get("/", tags=["Health"])
434
- async def root():
435
- """Health check endpoint"""
436
- return {
437
- "status": "healthy",
438
- "message": "Student Management API is running",
439
- "version": "1.0.0"
440
- }
441
-
442
- # ======================== RUN APPLICATION ========================
443
- if __name__ == "__main__":
444
- import uvicorn
445
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/voice.py DELETED
@@ -1,62 +0,0 @@
1
- import sounddevice as sd
2
- import numpy as np
3
- from faster_whisper import WhisperModel
4
- import ollama
5
- import subprocess
6
- import time
7
-
8
- # --- Configuration ---
9
- # STT Model: 'tiny' or 'base' are fast; 'small' is accurate.
10
- stt_model = WhisperModel("base.en", device="cuda", compute_type="float16")
11
-
12
- def record_audio(duration=5, samplerate=16000):
13
- """Simple recorder - in production, use VAD (Voice Activity Detection) to stop automatically."""
14
- print("Listening...")
15
- recording = sd.rec(int(duration * samplerate), samplerate=samplerate, channels=1, dtype='float32')
16
- sd.wait()
17
- return recording.flatten()
18
-
19
- def transcribe(audio_data):
20
- """Convert Audio to Text"""
21
- segments, info = stt_model.transcribe(audio_data, beam_size=5)
22
- text = " ".join([segment.text for segment in segments])
23
- return text.strip()
24
-
25
- def generate_response(prompt):
26
- """Send text to Ollama (The Brain)"""
27
- response = ollama.chat(model='llama3.2', messages=[
28
- {'role': 'user', 'content': prompt},
29
- ])
30
- return response['message']['content']
31
-
32
- def speak(text):
33
- """Convert Text to Audio (The Mouth) using Piper"""
34
- # Assuming you have the piper binary and a voice model downloaded
35
- # Command line: echo "text" | piper --model en_US-lessac-medium.onnx --output_file output.wav
36
-
37
- # For this example, we'll use a generic speak command for macOS/Linux
38
- # (Replace with Piper or pyttsx3 for true local/cross-platform)
39
- subprocess.call(["say", text])
40
-
41
- # --- Main Loop ---
42
- def main():
43
- print("Voice Agent Started (Ctrl+C to stop)")
44
- while True:
45
- # 1. Listen
46
- audio_data = record_audio(duration=4) # Fixed duration for simplicity
47
-
48
- # 2. Transcribe
49
- user_text = transcribe(audio_data)
50
- if not user_text: continue
51
-
52
- print(f"You: {user_text}")
53
-
54
- # 3. Think
55
- ai_response = generate_response(user_text)
56
- print(f"AI: {ai_response}")
57
-
58
- # 4. Speak
59
- speak(ai_response)
60
-
61
- if __name__ == "__main__":
62
- main()