Spaces:
Running
Running
feat: increase quiz generation capacity with dedicated high-token LLM.
Browse files- auth/routes.py +5 -1
- quiz_generator/constants.py +1 -1
- quiz_generator/quiz.py +8 -4
- quiz_generator/schemas.py +2 -2
- rag/rag.py +18 -1
- store.py +12 -24
- summary_generator/constants.py +0 -1
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":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 7 |
-
tf_count: int =
|
| 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=
|
| 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 =
|
| 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 |
-
|
| 650 |
-
check
|
| 651 |
-
|
| 652 |
"""
|
| 653 |
# Guard: never apply limit to admin emails
|
| 654 |
if email and email in ADMIN_EMAILS:
|
| 655 |
return True
|
| 656 |
|
| 657 |
-
|
| 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 =
|
| 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.
|