False45 commited on
Commit
4f811d2
·
1 Parent(s): 24a731d

Configure Firebase for production and prepare for deployment

Browse files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ serviceAccountKey.json
src/api/v1/chat.py CHANGED
@@ -141,8 +141,10 @@ async def chat_stream(request: Request, payload: ChatRequest, uid: str = Depends
141
  try:
142
  engine.llm = engine._init_llm() # Initialize Groq
143
 
144
- # Build simple prompt for Groq fallback
145
- system_text = "You are an advanced AI math tutor."
 
 
146
  parts = [{"role": "system", "content": system_text}]
147
  if context:
148
  parts.append({"role": "system", "content": f"Context: {context}"})
@@ -181,13 +183,15 @@ async def chat_stream(request: Request, payload: ChatRequest, uid: str = Depends
181
  @router.post("/vision/extract", response_model=VisionResponse)
182
  @limiter.limit("20/minute")
183
  async def extract_math_from_image(request: Request, file: UploadFile = File(...), solve: bool = Form(False), uid: str = Depends(verify_firebase_token), x_gemini_api_key: Optional[str] = Header(None)):
184
- """Extract math from an uploaded image using Gemini Vision."""
185
- if not settings.use_gemini:
186
- raise HTTPException(status_code=400, detail="Vision extraction requires Gemini API.")
187
-
188
  try:
189
  image_bytes = await file.read()
190
- vision_service = GeminiVisionService(custom_api_key=x_gemini_api_key)
 
 
 
 
 
191
 
192
  if solve:
193
  result = vision_service.extract_and_solve(image_bytes, mime_type=file.content_type)
 
141
  try:
142
  engine.llm = engine._init_llm() # Initialize Groq
143
 
144
+ from backend.src.config import SYSTEM_TEMPLATE
145
+
146
+ # Build simple prompt for Groq fallback using the master system template
147
+ system_text = SYSTEM_TEMPLATE
148
  parts = [{"role": "system", "content": system_text}]
149
  if context:
150
  parts.append({"role": "system", "content": f"Context: {context}"})
 
183
  @router.post("/vision/extract", response_model=VisionResponse)
184
  @limiter.limit("20/minute")
185
  async def extract_math_from_image(request: Request, file: UploadFile = File(...), solve: bool = Form(False), uid: str = Depends(verify_firebase_token), x_gemini_api_key: Optional[str] = Header(None)):
186
+ """Extract math from an uploaded image using Vision models."""
 
 
 
187
  try:
188
  image_bytes = await file.read()
189
+
190
+ if settings.use_gemini or x_gemini_api_key:
191
+ vision_service = GeminiVisionService(custom_api_key=x_gemini_api_key)
192
+ else:
193
+ from backend.src.services.gemini_service import GroqVisionService
194
+ vision_service = GroqVisionService()
195
 
196
  if solve:
197
  result = vision_service.extract_and_solve(image_bytes, mime_type=file.content_type)
src/api/v1/progress.py CHANGED
@@ -7,6 +7,7 @@ Backed by Firestore.
7
 
8
  import logging
9
  from datetime import datetime, timezone
 
10
  from pydantic import BaseModel
11
 
12
  from fastapi import APIRouter, Depends, HTTPException
@@ -22,6 +23,25 @@ class ProgressResponse(BaseModel):
22
  streak: int
23
  total_solved: int
24
  last_solved_date: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  @router.get("", response_model=ProgressResponse)
27
  async def get_progress(uid: str = Depends(verify_firebase_token)):
@@ -29,7 +49,8 @@ async def get_progress(uid: str = Depends(verify_firebase_token)):
29
  db = get_firestore_client()
30
  if not db:
31
  # Fallback if Firebase is disabled
32
- return ProgressResponse(streak=0, total_solved=0, last_solved_date="")
 
33
 
34
  try:
35
  doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
@@ -40,7 +61,10 @@ async def get_progress(uid: str = Depends(verify_firebase_token)):
40
  return ProgressResponse(
41
  streak=data.get("streak", 0),
42
  total_solved=data.get("total_solved", 0),
43
- last_solved_date=data.get("last_solved_date", "")
 
 
 
44
  )
45
  else:
46
  return ProgressResponse(streak=0, total_solved=0, last_solved_date="")
@@ -52,9 +76,32 @@ async def get_progress(uid: str = Depends(verify_firebase_token)):
52
  async def increment_progress(uid: str = Depends(verify_firebase_token)):
53
  """Increment the user's total solved count and update their streak."""
54
  db = get_firestore_client()
 
55
  if not db:
56
- return ProgressResponse(streak=1, total_solved=1, last_solved_date=datetime.now(timezone.utc).date().isoformat())
57
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
  doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
60
  today_str = datetime.now(timezone.utc).date().isoformat()
@@ -68,7 +115,12 @@ async def increment_progress(uid: str = Depends(verify_firebase_token)):
68
  data = {
69
  "streak": 1,
70
  "total_solved": 1,
71
- "last_solved_date": today
 
 
 
 
 
72
  }
73
  transaction.set(ref, data)
74
  return data
@@ -77,6 +129,9 @@ async def increment_progress(uid: str = Depends(verify_firebase_token)):
77
  last_date = current_data.get("last_solved_date", "")
78
  streak = current_data.get("streak", 0)
79
  total = current_data.get("total_solved", 0) + 1
 
 
 
80
 
81
  if last_date == today:
82
  # Already solved a problem today, just increment total
@@ -98,7 +153,10 @@ async def increment_progress(uid: str = Depends(verify_firebase_token)):
98
  data = {
99
  "streak": streak,
100
  "total_solved": total,
101
- "last_solved_date": today
 
 
 
102
  }
103
  transaction.update(ref, data)
104
  return data
@@ -110,6 +168,122 @@ async def increment_progress(uid: str = Depends(verify_firebase_token)):
110
  logger.error(f"Error incrementing progress for {uid}: {e}")
111
  raise HTTPException(status_code=500, detail=str(e))
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  def from_firestore_transaction(db):
114
  """Helper decorator for Firestore transactions."""
115
  from firebase_admin import firestore
 
7
 
8
  import logging
9
  from datetime import datetime, timezone
10
+ from typing import Dict, List
11
  from pydantic import BaseModel
12
 
13
  from fastapi import APIRouter, Depends, HTTPException
 
23
  streak: int
24
  total_solved: int
25
  last_solved_date: str
26
+ activity_map: Dict[str, int] = {}
27
+ weak_topics: List[str] = []
28
+ accuracy: int = 100
29
+
30
+ _MEMORY_STORE: Dict[str, dict] = {}
31
+
32
+ def get_memory_stats(uid: str) -> dict:
33
+ if uid not in _MEMORY_STORE:
34
+ _MEMORY_STORE[uid] = {
35
+ "streak": 0,
36
+ "total_solved": 0,
37
+ "last_solved_date": "",
38
+ "activity_map": {},
39
+ "weak_topics": [],
40
+ "accuracy": 100,
41
+ "correct_answers": 0,
42
+ "total_answers": 0
43
+ }
44
+ return _MEMORY_STORE[uid]
45
 
46
  @router.get("", response_model=ProgressResponse)
47
  async def get_progress(uid: str = Depends(verify_firebase_token)):
 
49
  db = get_firestore_client()
50
  if not db:
51
  # Fallback if Firebase is disabled
52
+ stats = get_memory_stats(uid)
53
+ return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__})
54
 
55
  try:
56
  doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
 
61
  return ProgressResponse(
62
  streak=data.get("streak", 0),
63
  total_solved=data.get("total_solved", 0),
64
+ last_solved_date=data.get("last_solved_date", ""),
65
+ activity_map=data.get("activity_map", {}),
66
+ weak_topics=data.get("weak_topics", []),
67
+ accuracy=data.get("accuracy", 100)
68
  )
69
  else:
70
  return ProgressResponse(streak=0, total_solved=0, last_solved_date="")
 
76
  async def increment_progress(uid: str = Depends(verify_firebase_token)):
77
  """Increment the user's total solved count and update their streak."""
78
  db = get_firestore_client()
79
+ today_str = datetime.now(timezone.utc).date().isoformat()
80
  if not db:
81
+ stats = get_memory_stats(uid)
82
+ last_date = stats.get("last_solved_date", "")
83
+ streak = stats.get("streak", 0)
84
+ total = stats.get("total_solved", 0) + 1
85
+ activity_map = stats.get("activity_map", {})
86
+ activity_map[today_str] = activity_map.get(today_str, 0) + 1
87
+
88
+ if last_date != today_str:
89
+ try:
90
+ if last_date:
91
+ delta = (datetime.fromisoformat(today_str).date() - datetime.fromisoformat(last_date).date()).days
92
+ streak = (streak + 1) if delta == 1 else 1
93
+ else:
94
+ streak = 1
95
+ except ValueError:
96
+ streak = 1
97
+
98
+ stats.update({
99
+ "streak": streak,
100
+ "total_solved": total,
101
+ "last_solved_date": today_str,
102
+ "activity_map": activity_map
103
+ })
104
+ return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__})
105
  try:
106
  doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
107
  today_str = datetime.now(timezone.utc).date().isoformat()
 
115
  data = {
116
  "streak": 1,
117
  "total_solved": 1,
118
+ "last_solved_date": today,
119
+ "activity_map": {today: 1},
120
+ "weak_topics": [],
121
+ "accuracy": 100,
122
+ "correct_answers": 1,
123
+ "total_answers": 1
124
  }
125
  transaction.set(ref, data)
126
  return data
 
129
  last_date = current_data.get("last_solved_date", "")
130
  streak = current_data.get("streak", 0)
131
  total = current_data.get("total_solved", 0) + 1
132
+ activity_map = current_data.get("activity_map", {})
133
+
134
+ activity_map[today] = activity_map.get(today, 0) + 1
135
 
136
  if last_date == today:
137
  # Already solved a problem today, just increment total
 
153
  data = {
154
  "streak": streak,
155
  "total_solved": total,
156
+ "last_solved_date": today,
157
+ "activity_map": activity_map,
158
+ "weak_topics": current_data.get("weak_topics", []),
159
+ "accuracy": current_data.get("accuracy", 100)
160
  }
161
  transaction.update(ref, data)
162
  return data
 
168
  logger.error(f"Error incrementing progress for {uid}: {e}")
169
  raise HTTPException(status_code=500, detail=str(e))
170
 
171
+ class WeaknessRequest(BaseModel):
172
+ topic: str
173
+
174
+ @router.post("/weakness", response_model=ProgressResponse)
175
+ async def add_weakness(req: WeaknessRequest, uid: str = Depends(verify_firebase_token)):
176
+ """Add a topic to the user's weak topics list."""
177
+ db = get_firestore_client()
178
+ if not db:
179
+ stats = get_memory_stats(uid)
180
+ topics = stats.get("weak_topics", [])
181
+ if req.topic not in topics:
182
+ topics.append(req.topic)
183
+ if len(topics) > 5:
184
+ topics = topics[-5:]
185
+ stats["weak_topics"] = topics
186
+ return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__})
187
+
188
+ doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
189
+
190
+ @from_firestore_transaction(db)
191
+ def update_weakness(transaction, ref):
192
+ snapshot = ref.get(transaction=transaction)
193
+ if not snapshot.exists:
194
+ data = {"streak": 0, "total_solved": 0, "last_solved_date": "", "weak_topics": [req.topic], "accuracy": 100}
195
+ transaction.set(ref, data)
196
+ return data
197
+
198
+ data = snapshot.to_dict()
199
+ topics = data.get("weak_topics", [])
200
+ if req.topic not in topics:
201
+ topics.append(req.topic)
202
+ # keep only last 5
203
+ if len(topics) > 5:
204
+ topics = topics[-5:]
205
+
206
+ update_data = {"weak_topics": topics}
207
+ transaction.update(ref, update_data)
208
+ data.update(update_data)
209
+ return data
210
+
211
+ try:
212
+ new_stats = update_weakness(db.transaction(), doc_ref)
213
+ return ProgressResponse(
214
+ streak=new_stats.get("streak", 0),
215
+ total_solved=new_stats.get("total_solved", 0),
216
+ last_solved_date=new_stats.get("last_solved_date", ""),
217
+ activity_map=new_stats.get("activity_map", {}),
218
+ weak_topics=new_stats.get("weak_topics", []),
219
+ accuracy=new_stats.get("accuracy", 100)
220
+ )
221
+ except Exception as e:
222
+ logger.error(f"Error updating weakness: {e}")
223
+ raise HTTPException(status_code=500, detail=str(e))
224
+
225
+ class AccuracyRequest(BaseModel):
226
+ correct: bool
227
+
228
+ @router.post("/accuracy", response_model=ProgressResponse)
229
+ async def update_accuracy(req: AccuracyRequest, uid: str = Depends(verify_firebase_token)):
230
+ """Update user's accuracy based on a correct/incorrect answer."""
231
+ db = get_firestore_client()
232
+ if not db:
233
+ stats = get_memory_stats(uid)
234
+ correct_ans = stats.get("correct_answers", 0) + (1 if req.correct else 0)
235
+ total_ans = stats.get("total_answers", 0) + 1
236
+ acc = int((correct_ans / total_ans) * 100)
237
+ stats.update({
238
+ "correct_answers": correct_ans,
239
+ "total_answers": total_ans,
240
+ "accuracy": acc
241
+ })
242
+ return ProgressResponse(**{k: v for k, v in stats.items() if k in ProgressResponse.__fields__})
243
+
244
+ doc_ref = db.collection("users").document(uid).collection("profile").document("stats")
245
+
246
+ @from_firestore_transaction(db)
247
+ def update_acc(transaction, ref):
248
+ snapshot = ref.get(transaction=transaction)
249
+ if not snapshot.exists:
250
+ data = {
251
+ "streak": 0, "total_solved": 0, "last_solved_date": "",
252
+ "correct_answers": 1 if req.correct else 0,
253
+ "total_answers": 1,
254
+ "accuracy": 100 if req.correct else 0
255
+ }
256
+ transaction.set(ref, data)
257
+ return data
258
+
259
+ data = snapshot.to_dict()
260
+ correct_ans = data.get("correct_answers", 0) + (1 if req.correct else 0)
261
+ total_ans = data.get("total_answers", 0) + 1
262
+ acc = int((correct_ans / total_ans) * 100)
263
+
264
+ update_data = {
265
+ "correct_answers": correct_ans,
266
+ "total_answers": total_ans,
267
+ "accuracy": acc
268
+ }
269
+ transaction.update(ref, update_data)
270
+ data.update(update_data)
271
+ return data
272
+
273
+ try:
274
+ new_stats = update_acc(db.transaction(), doc_ref)
275
+ return ProgressResponse(
276
+ streak=new_stats.get("streak", 0),
277
+ total_solved=new_stats.get("total_solved", 0),
278
+ last_solved_date=new_stats.get("last_solved_date", ""),
279
+ activity_map=new_stats.get("activity_map", {}),
280
+ weak_topics=new_stats.get("weak_topics", []),
281
+ accuracy=new_stats.get("accuracy", 100)
282
+ )
283
+ except Exception as e:
284
+ logger.error(f"Error updating accuracy: {e}")
285
+ raise HTTPException(status_code=500, detail=str(e))
286
+
287
  def from_firestore_transaction(db):
288
  """Helper decorator for Firestore transactions."""
289
  from firebase_admin import firestore
src/config.py CHANGED
@@ -117,7 +117,8 @@ setup_logging()
117
 
118
  SYSTEM_TEMPLATE = r"""
119
  You are an elite Indian mathematics teacher for Class 6 to 12 and JEE aspirants.
120
- Your goal is to explain math concepts and solve problems step-by-step.ly ONLY: "❌ I only teach math! Ask me any math problem."
 
121
  Stop immediately. Nothing else.
122
 
123
  ════════════════════════════════════════
 
117
 
118
  SYSTEM_TEMPLATE = r"""
119
  You are an elite Indian mathematics teacher for Class 6 to 12 and JEE aspirants.
120
+ Your goal is to explain math concepts and solve problems step-by-step.
121
+ If a user asks a non-math question, reply ONLY: "❌ I only teach math! Ask me any math problem."
122
  Stop immediately. Nothing else.
123
 
124
  ════════════════════════════════════════
src/services/gemini_service.py CHANGED
@@ -284,3 +284,16 @@ class GeminiVisionService:
284
  except Exception as e:
285
  logger.error(f"Extract and solve error: {e}")
286
  return {"extracted": "", "solution": f"⚠️ Error: {e}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  except Exception as e:
285
  logger.error(f"Extract and solve error: {e}")
286
  return {"extracted": "", "solution": f"⚠️ Error: {e}"}
287
+
288
+ class GroqVisionService:
289
+ def __init__(self):
290
+ # Groq has decommissioned their vision models. We provide a mock fallback for local testing.
291
+ pass
292
+
293
+ def extract_math_from_image(self, image_bytes: bytes, mime_type: str = "image/jpeg") -> str:
294
+ return "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}\n\n(Note: This is a mock response because Groq vision models are currently offline. Please add a Gemini API Key to use real vision extraction.)"
295
+
296
+ def extract_and_solve(self, image_bytes: bytes, mime_type: str = "image/jpeg") -> Dict[str, str]:
297
+ extracted = "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"
298
+ solution = "To solve this, we use the quadratic formula...\n\n(Note: This is a mock response because Groq vision models are currently offline. Please add a Gemini API Key to use real vision extraction.)"
299
+ return {"extracted": extracted, "solution": solution}
src/services/llm_service.py CHANGED
@@ -27,21 +27,81 @@ logger = logging.getLogger("math_assistant.llm")
27
  _LLM_CACHE = {}
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _get_llm(model=None):
31
  """Get or create a cached Groq LLM instance for a given model name."""
32
  key = model or settings.groq_model_fallbacks[0]
33
  if key not in _LLM_CACHE:
34
- from langchain_groq import ChatGroq
35
  api_key = settings.groq_api_key
36
  if not api_key:
37
  raise ValueError("GROQ_API_KEY not set. Add it to your .env file.")
38
- logger.info(f"Initializing Groq LLM: {key}")
39
- _LLM_CACHE[key] = ChatGroq(
40
- groq_api_key=api_key,
41
- model_name=key,
42
- temperature=0.1,
43
- max_tokens=2048,
44
- )
45
  return _LLM_CACHE[key]
46
 
47
 
 
27
  _LLM_CACHE = {}
28
 
29
 
30
+ class GroqLLMWrapper:
31
+ """Wrapper around native Groq client to mimic Langchain's invoke() and astream() methods."""
32
+ def __init__(self, api_key: str, model_name: str):
33
+ from groq import Groq, AsyncGroq
34
+ self.client = Groq(api_key=api_key)
35
+ self.async_client = AsyncGroq(api_key=api_key)
36
+ self.model_name = model_name
37
+
38
+ def invoke(self, messages):
39
+ from langchain_core.messages import AIMessage
40
+ groq_msgs = []
41
+
42
+ # If it's just a single string prompt
43
+ if isinstance(messages, str):
44
+ groq_msgs.append({"role": "user", "content": messages})
45
+ elif isinstance(messages, list):
46
+ for m in messages:
47
+ if isinstance(m, str):
48
+ groq_msgs.append({"role": "user", "content": m})
49
+ elif isinstance(m, dict):
50
+ # In case it's a raw dict
51
+ groq_msgs.append(m)
52
+ else:
53
+ role = "user" if getattr(m, "type", "user") == "human" else getattr(m, "type", "user")
54
+ content = m.content if isinstance(m.content, str) else str(m.content)
55
+ groq_msgs.append({"role": role, "content": content})
56
+
57
+ chat_completion = self.client.chat.completions.create(
58
+ messages=groq_msgs,
59
+ model=self.model_name,
60
+ temperature=0.1,
61
+ max_tokens=2048,
62
+ )
63
+
64
+ return AIMessage(content=chat_completion.choices[0].message.content)
65
+
66
+ async def astream(self, messages):
67
+ from langchain_core.messages import AIMessageChunk
68
+ groq_msgs = []
69
+
70
+ if isinstance(messages, str):
71
+ groq_msgs.append({"role": "user", "content": messages})
72
+ elif isinstance(messages, list):
73
+ for m in messages:
74
+ if isinstance(m, str):
75
+ groq_msgs.append({"role": "user", "content": m})
76
+ elif isinstance(m, dict):
77
+ groq_msgs.append(m)
78
+ else:
79
+ role = "user" if getattr(m, "type", "user") == "human" else getattr(m, "type", "user")
80
+ content = m.content if isinstance(m.content, str) else str(m.content)
81
+ groq_msgs.append({"role": role, "content": content})
82
+
83
+ stream = await self.async_client.chat.completions.create(
84
+ messages=groq_msgs,
85
+ model=self.model_name,
86
+ temperature=0.1,
87
+ max_tokens=2048,
88
+ stream=True
89
+ )
90
+
91
+ async for chunk in stream:
92
+ content = chunk.choices[0].delta.content
93
+ if content is not None:
94
+ yield AIMessageChunk(content=content)
95
+
96
  def _get_llm(model=None):
97
  """Get or create a cached Groq LLM instance for a given model name."""
98
  key = model or settings.groq_model_fallbacks[0]
99
  if key not in _LLM_CACHE:
 
100
  api_key = settings.groq_api_key
101
  if not api_key:
102
  raise ValueError("GROQ_API_KEY not set. Add it to your .env file.")
103
+ logger.info(f"Initializing Native Groq LLM: {key}")
104
+ _LLM_CACHE[key] = GroqLLMWrapper(api_key=api_key, model_name=key)
 
 
 
 
 
105
  return _LLM_CACHE[key]
106
 
107