Spaces:
Sleeping
Sleeping
github-actions[bot] commited on
Commit Β·
b2673e1
1
Parent(s): 647a656
Auto-deploy backend from GitHub Actions
Browse files- app/api/v1/endpoints/learn.py +25 -2
- app/schemas/cards.py +4 -0
- app/services/gamification_service.py +22 -3
- app/services/llm_service.py +28 -4
app/api/v1/endpoints/learn.py
CHANGED
|
@@ -16,11 +16,32 @@ async def create_learning_deck(
|
|
| 16 |
request: LearningDeckRequest,
|
| 17 |
db_data: tuple[Client, str] = Depends(get_user_db_client),
|
| 18 |
):
|
|
|
|
| 19 |
try:
|
| 20 |
-
# 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
existing_skills = await get_existing_skills_for_strand(request.chosen_lens)
|
| 22 |
|
| 23 |
-
# 2. Generate the 3-Card Deck using Gemini
|
| 24 |
deck = await generate_learning_deck(
|
| 25 |
object_name=request.object_name,
|
| 26 |
strand=request.chosen_lens,
|
|
@@ -29,6 +50,8 @@ async def create_learning_deck(
|
|
| 29 |
existing_skills=existing_skills,
|
| 30 |
)
|
| 31 |
|
|
|
|
|
|
|
| 32 |
return deck
|
| 33 |
|
| 34 |
except HTTPException:
|
|
|
|
| 16 |
request: LearningDeckRequest,
|
| 17 |
db_data: tuple[Client, str] = Depends(get_user_db_client),
|
| 18 |
):
|
| 19 |
+
db_client, user_id = db_data
|
| 20 |
try:
|
| 21 |
+
# --- 1. MEMORY CHECK (ANTI-CHEAT) ---
|
| 22 |
+
# Did the user already scan this exact object under this exact lens?
|
| 23 |
+
past_scan = (
|
| 24 |
+
db_client.table("scans")
|
| 25 |
+
.select("learning_deck")
|
| 26 |
+
.eq("user_id", user_id)
|
| 27 |
+
.eq("object_name", request.object_name)
|
| 28 |
+
.eq("chosen_lens", request.chosen_lens)
|
| 29 |
+
.execute()
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
if past_scan.data:
|
| 33 |
+
# DUPLICATE FOUND: Return the old deck, bypass Gemini, save API costs!
|
| 34 |
+
saved_deck = past_scan.data[0]["learning_deck"]
|
| 35 |
+
return LearningDeckResponse(
|
| 36 |
+
concept_card=saved_deck.get("concept_card", {}),
|
| 37 |
+
real_world_card=saved_deck.get("real_world_card", {}),
|
| 38 |
+
challenge_card=saved_deck.get("challenge_card", {}),
|
| 39 |
+
is_memory=True, # π Flag for the mobile UI
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# --- 2. NEW DISCOVERY (Proceed normally) ---
|
| 43 |
existing_skills = await get_existing_skills_for_strand(request.chosen_lens)
|
| 44 |
|
|
|
|
| 45 |
deck = await generate_learning_deck(
|
| 46 |
object_name=request.object_name,
|
| 47 |
strand=request.chosen_lens,
|
|
|
|
| 50 |
existing_skills=existing_skills,
|
| 51 |
)
|
| 52 |
|
| 53 |
+
# Ensure the flag is false for new generations
|
| 54 |
+
deck.is_memory = False
|
| 55 |
return deck
|
| 56 |
|
| 57 |
except HTTPException:
|
app/schemas/cards.py
CHANGED
|
@@ -54,3 +54,7 @@ class LearningDeckResponse(BaseModel):
|
|
| 54 |
concept_card: CardConcept
|
| 55 |
real_world_card: CardRealWorld
|
| 56 |
challenge_card: CardChallenge
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
concept_card: CardConcept
|
| 55 |
real_world_card: CardRealWorld
|
| 56 |
challenge_card: CardChallenge
|
| 57 |
+
is_memory: bool = Field(
|
| 58 |
+
default=False,
|
| 59 |
+
description="True if retrieved from DB, False if newly generated by AI.",
|
| 60 |
+
)
|
app/services/gamification_service.py
CHANGED
|
@@ -14,8 +14,28 @@ def save_user_discovery(
|
|
| 14 |
) -> tuple[str, int]:
|
| 15 |
"""
|
| 16 |
Saves the finalized scan, and triggers the Postgres RPC to update Streaks, XP, and the Skill Tree.
|
|
|
|
| 17 |
"""
|
| 18 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# 1. Calculate actual XP server-side
|
| 20 |
final_xp = (
|
| 21 |
BASE_XP_PER_SCAN * 2
|
|
@@ -30,7 +50,7 @@ def save_user_discovery(
|
|
| 30 |
"chosen_lens": request.chosen_lens,
|
| 31 |
"image_url": request.image_url,
|
| 32 |
"learning_deck": request.learning_deck,
|
| 33 |
-
"xp_awarded": final_xp,
|
| 34 |
"is_aligned_with_compass": request.is_aligned_with_compass,
|
| 35 |
}
|
| 36 |
|
|
@@ -42,7 +62,7 @@ def save_user_discovery(
|
|
| 42 |
|
| 43 |
scan_id = response.data[0]["id"]
|
| 44 |
|
| 45 |
-
# 4. π
|
| 46 |
db_client.rpc(
|
| 47 |
"award_xp_and_update_streak",
|
| 48 |
{
|
|
@@ -53,7 +73,6 @@ def save_user_discovery(
|
|
| 53 |
},
|
| 54 |
).execute()
|
| 55 |
|
| 56 |
-
# π FIX: Return both the ID and the calculated XP securely
|
| 57 |
return scan_id, final_xp
|
| 58 |
|
| 59 |
except Exception as e:
|
|
|
|
| 14 |
) -> tuple[str, int]:
|
| 15 |
"""
|
| 16 |
Saves the finalized scan, and triggers the Postgres RPC to update Streaks, XP, and the Skill Tree.
|
| 17 |
+
Includes strict Anti-Cheat validation.
|
| 18 |
"""
|
| 19 |
try:
|
| 20 |
+
# --- ANTI-CHEAT DB CHECK ---
|
| 21 |
+
# Prevent hackers from hitting /save repeatedly for the same object+lens
|
| 22 |
+
past_scan = (
|
| 23 |
+
db_client.table("scans")
|
| 24 |
+
.select("id")
|
| 25 |
+
.eq("user_id", user_id)
|
| 26 |
+
.eq("object_name", request.object_name)
|
| 27 |
+
.eq("chosen_lens", request.chosen_lens)
|
| 28 |
+
.execute()
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
if past_scan.data:
|
| 32 |
+
# They already did this. Return the old ID and 0 XP. Do not insert duplicate data.
|
| 33 |
+
logger.warning(
|
| 34 |
+
f"Anti-Cheat: User {user_id} attempted to save duplicate scan ({request.object_name} + {request.chosen_lens})."
|
| 35 |
+
)
|
| 36 |
+
return past_scan.data[0]["id"], 0
|
| 37 |
+
|
| 38 |
+
# --- PROCEED WITH NEW SAVE ---
|
| 39 |
# 1. Calculate actual XP server-side
|
| 40 |
final_xp = (
|
| 41 |
BASE_XP_PER_SCAN * 2
|
|
|
|
| 50 |
"chosen_lens": request.chosen_lens,
|
| 51 |
"image_url": request.image_url,
|
| 52 |
"learning_deck": request.learning_deck,
|
| 53 |
+
"xp_awarded": final_xp,
|
| 54 |
"is_aligned_with_compass": request.is_aligned_with_compass,
|
| 55 |
}
|
| 56 |
|
|
|
|
| 62 |
|
| 63 |
scan_id = response.data[0]["id"]
|
| 64 |
|
| 65 |
+
# 4. π Execute the Postgres Function to update Streaks and Profile XP
|
| 66 |
db_client.rpc(
|
| 67 |
"award_xp_and_update_streak",
|
| 68 |
{
|
|
|
|
| 73 |
},
|
| 74 |
).execute()
|
| 75 |
|
|
|
|
| 76 |
return scan_id, final_xp
|
| 77 |
|
| 78 |
except Exception as e:
|
app/services/llm_service.py
CHANGED
|
@@ -43,7 +43,6 @@ async def generate_discovery_from_image(
|
|
| 43 |
) -> DiscoverLLMResponse:
|
| 44 |
try:
|
| 45 |
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
| 46 |
-
# Now passing the context
|
| 47 |
prompt_text = VISION_DISCOVERY_PROMPT.format(
|
| 48 |
grade_level=grade_level, active_quests_context=active_quests_context
|
| 49 |
)
|
|
@@ -65,12 +64,28 @@ async def generate_discovery_from_image(
|
|
| 65 |
except asyncio.TimeoutError:
|
| 66 |
raise HTTPException(
|
| 67 |
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
| 68 |
-
detail="Vision AI timed out while analyzing the image. The
|
| 69 |
)
|
| 70 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
raise HTTPException(
|
| 72 |
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 73 |
-
detail=f"AI Vision Processing Failed: {
|
| 74 |
)
|
| 75 |
|
| 76 |
|
|
@@ -96,9 +111,18 @@ async def generate_holistic_pathfinder(
|
|
| 96 |
detail="Pathfinder AI timed out. Please try again.",
|
| 97 |
)
|
| 98 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
raise HTTPException(
|
| 100 |
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 101 |
-
detail=f"Pathfinder generation failed: {
|
| 102 |
)
|
| 103 |
|
| 104 |
|
|
|
|
| 43 |
) -> DiscoverLLMResponse:
|
| 44 |
try:
|
| 45 |
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
|
|
| 46 |
prompt_text = VISION_DISCOVERY_PROMPT.format(
|
| 47 |
grade_level=grade_level, active_quests_context=active_quests_context
|
| 48 |
)
|
|
|
|
| 64 |
except asyncio.TimeoutError:
|
| 65 |
raise HTTPException(
|
| 66 |
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
| 67 |
+
detail="Vision AI timed out while analyzing the image. The network might be slow.",
|
| 68 |
)
|
| 69 |
except Exception as e:
|
| 70 |
+
error_str = str(e)
|
| 71 |
+
|
| 72 |
+
# 1. Handle Rate Limits
|
| 73 |
+
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str:
|
| 74 |
+
raise HTTPException(
|
| 75 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 76 |
+
detail="Scanner network congested. Please wait a few seconds and try scanning again.",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# 2. Handle Inappropriate Objects (NSFW, Violence, etc.)
|
| 80 |
+
if "SAFETY" in error_str or "FinishReason.SAFETY" in error_str:
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 83 |
+
detail="System Override: Unidentified or Restricted Anomaly Detected. Scan Aborted.",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
raise HTTPException(
|
| 87 |
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 88 |
+
detail=f"AI Vision Processing Failed: {error_str}",
|
| 89 |
)
|
| 90 |
|
| 91 |
|
|
|
|
| 111 |
detail="Pathfinder AI timed out. Please try again.",
|
| 112 |
)
|
| 113 |
except Exception as e:
|
| 114 |
+
error_str = str(e)
|
| 115 |
+
|
| 116 |
+
# 1. Handle Rate Limits
|
| 117 |
+
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str:
|
| 118 |
+
raise HTTPException(
|
| 119 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 120 |
+
detail="Pathfinder generation is currently queued. Please try again in a moment.",
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
raise HTTPException(
|
| 124 |
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 125 |
+
detail=f"Pathfinder generation failed: {error_str}",
|
| 126 |
)
|
| 127 |
|
| 128 |
|