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 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. Fetch existing skills from Neo4j to enforce Data Governance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, # Use the secure server calculated 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. πŸš€ CRITICAL: Execute the Postgres Function to update Streaks and Profile XP
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 image might be too complex or the network is slow.",
69
  )
70
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  raise HTTPException(
72
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
73
- detail=f"AI Vision Processing Failed: {str(e)}",
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: {str(e)}",
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