Hamdy005 commited on
Commit
6a2fe0d
Β·
1 Parent(s): b2dcea0

feat: increase quiz generation capacity with dedicated high-token LLM.

Browse files
auth/routes.py CHANGED
@@ -94,7 +94,11 @@ async def get_profile(
94
  "email": getattr(user_obj, "email", "") or "",
95
  "avatar_url": "",
96
  "daily_requests": real_usage.get("used", 0),
97
- "last_request_date": __import__('datetime').date.today().isoformat(),
 
 
 
 
98
  "_is_fallback": True,
99
  })
100
 
 
94
  "email": getattr(user_obj, "email", "") or "",
95
  "avatar_url": "",
96
  "daily_requests": real_usage.get("used", 0),
97
+ "last_request_date": (
98
+ __import__('datetime').datetime.now(
99
+ __import__('datetime').timezone(__import__('datetime').timedelta(hours=3))
100
+ ).date().isoformat()
101
+ ),
102
  "_is_fallback": True,
103
  })
104
 
quiz_generator/constants.py CHANGED
@@ -1,7 +1,7 @@
1
  from langchain.prompts import PromptTemplate
2
 
3
  MIN_MCQ_COUNT = 1
4
- MAX_MCQ_COUNT = 20
5
  MIN_TF_COUNT = 1
6
  MAX_TF_COUNT = 20
7
 
 
1
  from langchain.prompts import PromptTemplate
2
 
3
  MIN_MCQ_COUNT = 1
4
+ MAX_MCQ_COUNT = 40
5
  MIN_TF_COUNT = 1
6
  MAX_TF_COUNT = 20
7
 
quiz_generator/quiz.py CHANGED
@@ -6,7 +6,7 @@ from typing import Optional
6
  from langchain.agents import create_tool_calling_agent, AgentExecutor
7
  from langchain_core.tools import create_retriever_tool
8
 
9
- from src.rag.rag import get_llm, web_search_tools, SupabaseRetriever
10
  from .constants import (
11
  QUIZ_PROMPT_TEMPLATE,
12
  WEB_QUIZ_PROMPT_TEMPLATE,
@@ -60,7 +60,7 @@ def _summary_quiz(difficulty, mcq_count, tf_count, context_text):
60
  logger.info(f"Summary Quiz started (diff={difficulty}, mcq={mcq_count}, tf={tf_count})")
61
  try:
62
  prompt = _quiz_prompt()
63
- llm = get_llm()
64
  safe_context = context_text
65
 
66
  chain = prompt | llm
@@ -87,7 +87,7 @@ def _contextual_quiz(difficulty, mcq_count, tf_count, context, material_id):
87
  logger.info(f"Contextual Quiz started (material_id={material_id}, diff={difficulty})")
88
  try:
89
  prompt = _quiz_prompt()
90
- llm = get_llm()
91
 
92
  retriever = SupabaseRetriever(material_id=material_id, k=RETRIEVER_K)
93
  retriever_tool = create_retriever_tool(
@@ -103,6 +103,8 @@ def _contextual_quiz(difficulty, mcq_count, tf_count, context, material_id):
103
  verbose=False,
104
  return_intermediate_steps=False,
105
  handle_parsing_errors=True,
 
 
106
  )
107
 
108
  safe_context = context or ""
@@ -125,7 +127,7 @@ def _web_quiz(difficulty, mcq_count, tf_count, topic_title):
125
  logger.info(f"Web Quiz started (topic={topic_title}, diff={difficulty})")
126
  try:
127
  prompt = WEB_QUIZ_PROMPT_TEMPLATE
128
- llm = get_llm()
129
  tools = web_search_tools(
130
  wiki_k=WIKI_TOP_K_RESULTS,
131
  wiki_chars=WIKI_DOC_CONTENT_CHARS_MAX,
@@ -140,6 +142,8 @@ def _web_quiz(difficulty, mcq_count, tf_count, topic_title):
140
  verbose=False,
141
  return_intermediate_steps=False,
142
  handle_parsing_errors=True,
 
 
143
  )
144
 
145
  safe_context = topic_title
 
6
  from langchain.agents import create_tool_calling_agent, AgentExecutor
7
  from langchain_core.tools import create_retriever_tool
8
 
9
+ from src.rag.rag import get_quiz_llm, web_search_tools, SupabaseRetriever
10
  from .constants import (
11
  QUIZ_PROMPT_TEMPLATE,
12
  WEB_QUIZ_PROMPT_TEMPLATE,
 
60
  logger.info(f"Summary Quiz started (diff={difficulty}, mcq={mcq_count}, tf={tf_count})")
61
  try:
62
  prompt = _quiz_prompt()
63
+ llm = get_quiz_llm()
64
  safe_context = context_text
65
 
66
  chain = prompt | llm
 
87
  logger.info(f"Contextual Quiz started (material_id={material_id}, diff={difficulty})")
88
  try:
89
  prompt = _quiz_prompt()
90
+ llm = get_quiz_llm()
91
 
92
  retriever = SupabaseRetriever(material_id=material_id, k=RETRIEVER_K)
93
  retriever_tool = create_retriever_tool(
 
103
  verbose=False,
104
  return_intermediate_steps=False,
105
  handle_parsing_errors=True,
106
+ max_iterations=80,
107
+ max_execution_time=300,
108
  )
109
 
110
  safe_context = context or ""
 
127
  logger.info(f"Web Quiz started (topic={topic_title}, diff={difficulty})")
128
  try:
129
  prompt = WEB_QUIZ_PROMPT_TEMPLATE
130
+ llm = get_quiz_llm()
131
  tools = web_search_tools(
132
  wiki_k=WIKI_TOP_K_RESULTS,
133
  wiki_chars=WIKI_DOC_CONTENT_CHARS_MAX,
 
142
  verbose=False,
143
  return_intermediate_steps=False,
144
  handle_parsing_errors=True,
145
+ max_iterations=80,
146
+ max_execution_time=300,
147
  )
148
 
149
  safe_context = topic_title
quiz_generator/schemas.py CHANGED
@@ -3,8 +3,8 @@ from typing import Optional
3
 
4
  class QuizRequest(BaseModel):
5
  difficulty: str = "Medium"
6
- mcq_count: int = 4
7
- tf_count: int = 3
8
  source_type: str = "web"
9
  material_id: Optional[str] = None
10
  topic: Optional[str] = None
 
3
 
4
  class QuizRequest(BaseModel):
5
  difficulty: str = "Medium"
6
+ mcq_count: int = 10
7
+ tf_count: int = 5
8
  source_type: str = "web"
9
  material_id: Optional[str] = None
10
  topic: Optional[str] = None
rag/rag.py CHANGED
@@ -146,11 +146,28 @@ def get_llm():
146
  model=settings.model_name,
147
  api_key=settings.gemini_api_key,
148
  temperature=0.3,
149
- max_output_tokens=2000,
150
  timeout=120,
151
  )
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def get_groq_llm():
155
  if not settings.groq_api_key:
156
  raise ValueError("GROQ_API_KEY not found. Please set it in config.env.")
 
146
  model=settings.model_name,
147
  api_key=settings.gemini_api_key,
148
  temperature=0.3,
149
+ max_output_tokens=2500,
150
  timeout=120,
151
  )
152
 
153
 
154
+ def get_quiz_llm():
155
+ """Dedicated LLM instance for quiz generation with a high output token budget.
156
+ Large quizzes (40 MCQs + 20 T/F) can produce 8000-12000 tokens of JSON,
157
+ so we cannot reuse the chat LLM which is capped at 2000 tokens.
158
+ """
159
+ if not os.environ.get("GEMINI_API_KEY"):
160
+ raise ValueError("GEMINI_API_KEY not found. Please set it in config.env.")
161
+ logger.info(f"Initializing Quiz LLM with model: {settings.model_name}")
162
+ return ChatGoogleGenerativeAI(
163
+ model=settings.model_name,
164
+ api_key=settings.gemini_api_key,
165
+ temperature=0.3,
166
+ max_output_tokens=16000,
167
+ timeout=300,
168
+ )
169
+
170
+
171
  def get_groq_llm():
172
  if not settings.groq_api_key:
173
  raise ValueError("GROQ_API_KEY not found. Please set it in config.env.")
store.py CHANGED
@@ -3,10 +3,15 @@ import time
3
  from httpx import RemoteProtocolError
4
  from typing import Optional
5
  import logging
6
- from datetime import datetime, timezone, date
7
  from langchain.memory import ConversationBufferMemory, ConversationBufferWindowMemory
8
  from src.database import get_supabase
9
 
 
 
 
 
 
10
  _in_memory: dict = {
11
  "materials": {},
12
  "material_chunks": {},
@@ -384,7 +389,7 @@ def get_quizzes(material_id: Optional[str] = None, user_id: Optional[str] = None
384
  # ── Users (maps to Supabase `profiles` table) ─────────
385
 
386
  def _map_profile(profile: dict) -> dict:
387
- today = date.today().isoformat()
388
  used = profile.get("daily_requests", 0) if profile.get("last_request_date") == today else 0
389
  return {
390
  "id": profile["id"],
@@ -646,32 +651,15 @@ def check_and_increment_daily_limit(user_id: str, email: Optional[str] = None, l
646
  Returns True if request is allowed, False if limit exceeded.
647
  Excludes Admin Emails from Limits.
648
 
649
- Uses a single atomic SQL UPDATE via Supabase RPC to eliminate the
650
- check-then-increment race condition (two concurrent reads both see
651
- count=9, both pass, both increment β†’ 11th request gets through).
652
  """
653
  # Guard: never apply limit to admin emails
654
  if email and email in ADMIN_EMAILS:
655
  return True
656
 
657
- client = _db()
658
- if client is not None:
659
- try:
660
- result = client.rpc(
661
- "increment_daily_limit",
662
- {"p_user_id": user_id, "p_limit": limit},
663
- ).execute()
664
- if not result.data:
665
- return False
666
- return True
667
- except Exception as e:
668
- logger.warning(f"Atomic rate-limit RPC failed, falling back to two-query path: {e}")
669
- # Fall through to the two-query fallback below
670
-
671
- # ── Offline / dev fallback (also used when RPC call above raises) ──────────
672
- # This path has a theoretical race condition but is acceptable for single-
673
- # worker dev environments where the RPC is not available.
674
- today = date.today().isoformat()
675
  try:
676
  result = _robust_execute(
677
  _table_supabase("profiles")
@@ -706,7 +694,7 @@ def get_usage(user_id: str) -> dict:
706
  """
707
  Returns current usage for a user.
708
  """
709
- today = date.today().isoformat()
710
  try:
711
  result = _robust_execute(
712
  _table_supabase("profiles")
 
3
  from httpx import RemoteProtocolError
4
  from typing import Optional
5
  import logging
6
+ from datetime import datetime, timezone, date, timedelta
7
  from langchain.memory import ConversationBufferMemory, ConversationBufferWindowMemory
8
  from src.database import get_supabase
9
 
10
+ def _get_today_date_str() -> str:
11
+ # Shift UTC time by 3 hours to match Egypt timezone (UTC+3), so daily limits reset at 12 AM Egypt time.
12
+ egypt_tz = timezone(timedelta(hours=3))
13
+ return datetime.now(egypt_tz).date().isoformat()
14
+
15
  _in_memory: dict = {
16
  "materials": {},
17
  "material_chunks": {},
 
389
  # ── Users (maps to Supabase `profiles` table) ─────────
390
 
391
  def _map_profile(profile: dict) -> dict:
392
+ today = _get_today_date_str()
393
  used = profile.get("daily_requests", 0) if profile.get("last_request_date") == today else 0
394
  return {
395
  "id": profile["id"],
 
651
  Returns True if request is allowed, False if limit exceeded.
652
  Excludes Admin Emails from Limits.
653
 
654
+ Bypasses the database RPC (which runs in UTC and resets at 3 AM Egypt time)
655
+ to perform the date check in python using Egypt local timezone (UTC+3),
656
+ ensuring limits reset exactly at 12 AM Egypt time.
657
  """
658
  # Guard: never apply limit to admin emails
659
  if email and email in ADMIN_EMAILS:
660
  return True
661
 
662
+ today = _get_today_date_str()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  try:
664
  result = _robust_execute(
665
  _table_supabase("profiles")
 
694
  """
695
  Returns current usage for a user.
696
  """
697
+ today = _get_today_date_str()
698
  try:
699
  result = _robust_execute(
700
  _table_supabase("profiles")
summary_generator/constants.py CHANGED
@@ -104,7 +104,6 @@ Analyze the content and produce a summary with exactly these five sections in th
104
  - **Historical Background** (e.g., [[[[>>> Historical Background <<<]]]]) β€” Brief, concise history: when it originated, key milestones, and major contributors. Keep this summarized.
105
  - **Core Concepts and Fundamentals** (e.g., [[[[>>> Core Concepts and Fundamentals <<<]]]]) β€” The essential principles, mechanisms, theories, or ideas that form the foundation of this topic.
106
  - **Types / Categories / Variants** (e.g., [[[[>>> Types and Classifications <<<]]]]) β€” If the topic has distinct types, classifications, branches, or variants, list and briefly explain each one.
107
- - **Architecture / Structure / Components** (e.g., [[[[>>> Architecture and Components <<<]]]]) β€” If applicable, describe the internal structure, architecture, system design, or key components and how they relate.
108
  - **How It Works / Process / Mechanism** (e.g., [[[[>>> How It Works <<<]]]]) β€” Step-by-step explanation of how it functions, operates, or proceeds, if applicable.
109
  - **Applications and Use Cases** (e.g., [[[[>>> Applications and Use Cases <<<]]]]) β€” Real-world applications, practical uses, and examples of where this topic is applied.
110
  - **Advantages and Strengths** (e.g., [[[[>>> Advantages and Strengths <<<]]]]) β€” Key benefits, strengths, and reasons why this topic/approach is valuable.
 
104
  - **Historical Background** (e.g., [[[[>>> Historical Background <<<]]]]) β€” Brief, concise history: when it originated, key milestones, and major contributors. Keep this summarized.
105
  - **Core Concepts and Fundamentals** (e.g., [[[[>>> Core Concepts and Fundamentals <<<]]]]) β€” The essential principles, mechanisms, theories, or ideas that form the foundation of this topic.
106
  - **Types / Categories / Variants** (e.g., [[[[>>> Types and Classifications <<<]]]]) β€” If the topic has distinct types, classifications, branches, or variants, list and briefly explain each one.
 
107
  - **How It Works / Process / Mechanism** (e.g., [[[[>>> How It Works <<<]]]]) β€” Step-by-step explanation of how it functions, operates, or proceeds, if applicable.
108
  - **Applications and Use Cases** (e.g., [[[[>>> Applications and Use Cases <<<]]]]) β€” Real-world applications, practical uses, and examples of where this topic is applied.
109
  - **Advantages and Strengths** (e.g., [[[[>>> Advantages and Strengths <<<]]]]) β€” Key benefits, strengths, and reasons why this topic/approach is valuable.