rairo commited on
Commit
9f00247
·
verified ·
1 Parent(s): 8b5827f

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +287 -1
main.py CHANGED
@@ -1648,8 +1648,294 @@ def image_proxy():
1648
  return jsonify({"error": "Internal server error processing the image request."}), 500
1649
 
1650
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1651
  # -----------------------------------------------------------------------------
1652
- # 11. MAIN
1653
  # -----------------------------------------------------------------------------
1654
 
1655
  if __name__ == "__main__":
 
1648
  return jsonify({"error": "Internal server error processing the image request."}), 500
1649
 
1650
 
1651
+ # =============================================================================
1652
+ # 11. LEARNING PATH MODULE (NEW ADDITION)
1653
+ # =============================================================================
1654
+ # This module implements the "Overlay Architecture".
1655
+ # It fetches static structure from the Data API and overlays user progress from Firebase.
1656
+
1657
+ # --- Configuration ---
1658
+
1659
+ DATA_API_BASE_URL = "https://rairo-marka-data-api.hf.space/v1"
1660
+
1661
+ # Registry mapping your App IDs to the Data API Filename IDs (Based on your screenshots)
1662
+ PATH_SUBJECT_MAP = {
1663
+ # --- A Level ---
1664
+ "al_acc": "A_9706", # Accounting
1665
+ "al_bio": "A_9700", # Biology
1666
+ "al_bus": "A_9609", # Business
1667
+ "al_chem": "A_9701", # Chemistry
1668
+ "al_cs": "A_9618", # Computer Science
1669
+ "al_eco": "A_9708", # Economics
1670
+ "al_fmath": "A_9231", # Further Mathematics
1671
+ "al_hist": "A_9489", # History
1672
+ "al_lit": "A_9695", # Literature in English
1673
+ "al_math": "A_9709", # Mathematics
1674
+ "al_phy": "A_9702", # Physics
1675
+ "al_soc": "A_9699", # Sociology
1676
+ "al_travel": "A_9395", # Travel & Tourism
1677
+
1678
+ # --- O Level (IGCSE) ---
1679
+ "ol_acc": "O_0452", # Accounting
1680
+ "ol_bio": "O_0610", # Biology
1681
+ "ol_bus": "O_0450", # Business Studies
1682
+ "ol_chem": "O_0620", # Chemistry
1683
+ "ol_cs": "O_0478", # Computer Science
1684
+ "ol_eng": "O_0500", # English Language
1685
+ "ol_lit": "O_0475", # English Literature
1686
+ "ol_env": "O_0680", # Environmental Management
1687
+ "ol_geo": "O_0460", # Geography
1688
+ "ol_hist": "O_0470", # History
1689
+ "ol_phy": "O_0625", # Physics
1690
+ }
1691
+
1692
+ # --- Helpers ---
1693
+
1694
+ def fetch_remote_syllabus(remote_id):
1695
+ """Fetches the static JSON tree from the Data API."""
1696
+ try:
1697
+ url = f"{DATA_API_BASE_URL}/structure/{remote_id}"
1698
+ resp = requests.get(url, timeout=10)
1699
+ if resp.status_code == 200:
1700
+ return resp.json()
1701
+ logger.error(f"Data API Error {resp.status_code}: {resp.text}")
1702
+ return None
1703
+ except Exception as e:
1704
+ logger.error(f"Failed to connect to Data API: {e}")
1705
+ return None
1706
+
1707
+ def search_remote_context(query, remote_id):
1708
+ """Searches the Data API for specific syllabus content to ground the AI."""
1709
+ try:
1710
+ url = f"{DATA_API_BASE_URL}/search"
1711
+ payload = {
1712
+ "query": query,
1713
+ "filter_subject_id": remote_id
1714
+ }
1715
+ resp = requests.post(url, json=payload, timeout=10)
1716
+ if resp.status_code == 200:
1717
+ return resp.json().get("results", [])
1718
+ return []
1719
+ except Exception as e:
1720
+ logger.error(f"Context search failed: {e}")
1721
+ return []
1722
+
1723
+ # --- Endpoints ---
1724
+
1725
+ @app.route("/api/path/tree", methods=["GET"])
1726
+ def get_path_tree():
1727
+ """
1728
+ Returns the full syllabus tree with 'locked', 'unlocked', or 'completed' status.
1729
+ Query Param: subjectId (e.g., 'al_phy')
1730
+ """
1731
+ uid = verify_token(request.headers.get("Authorization"))
1732
+ if not uid:
1733
+ return jsonify({"error": "Unauthorized"}), 401
1734
+
1735
+ subject_id = request.args.get("subjectId")
1736
+ if not subject_id or subject_id not in PATH_SUBJECT_MAP:
1737
+ return jsonify({"error": "Invalid or unsupported subjectId"}), 400
1738
+
1739
+ remote_id = PATH_SUBJECT_MAP[subject_id]
1740
+
1741
+ # 1. Get Static Map
1742
+ syllabus_data = fetch_remote_syllabus(remote_id)
1743
+ if not syllabus_data:
1744
+ return jsonify({"error": "Syllabus data currently unavailable"}), 503
1745
+
1746
+ # 2. Get User Progress
1747
+ # Stored as: users/{uid}/path_progress/{subject_id}/{node_id} = timestamp
1748
+ progress_ref = db_ref.child(f"users/{uid}/path_progress/{subject_id}")
1749
+ user_progress = progress_ref.get() or {}
1750
+
1751
+ # 3. Recursive Overlay Logic
1752
+ def enrich_node(node, parent_is_completed):
1753
+ node_id = node.get("id")
1754
+
1755
+ # Check completion status
1756
+ completed_timestamp = user_progress.get(node_id)
1757
+ is_completed = completed_timestamp is not None
1758
+
1759
+ # Determine State
1760
+ status = "locked"
1761
+ if is_completed:
1762
+ status = "completed"
1763
+ elif parent_is_completed:
1764
+ # If parent is done (or it's a root node), this is the next step
1765
+ status = "unlocked"
1766
+
1767
+ enriched = {
1768
+ "id": node_id,
1769
+ "title": node.get("title"),
1770
+ "type": node.get("type"),
1771
+ "status": status,
1772
+ "isCompleted": is_completed
1773
+ }
1774
+
1775
+ # Handle Children
1776
+ if "children" in node:
1777
+ # Children are only accessible if THIS node is completed
1778
+ # (Or if it's a container 'topic' that doesn't require work itself, pass parent state)
1779
+ # Strategy: Topics (Folders) are 'completed' if any child is done?
1780
+ # Simple Strategy: Flow down. Children unlock if parent is completed.
1781
+ enriched["children"] = [
1782
+ enrich_node(child, parent_is_completed=is_completed or (node.get("type") == "topic" and parent_is_completed))
1783
+ for child in node["children"]
1784
+ ]
1785
+
1786
+ return enriched
1787
+
1788
+ # 4. Process Root
1789
+ # The API returns { meta: {...}, tree: [...] }
1790
+ raw_tree = syllabus_data.get("tree", [])
1791
+ # Root nodes are always unlocked
1792
+ enriched_tree = [enrich_node(root, parent_is_completed=True) for root in raw_tree]
1793
+
1794
+ return jsonify({
1795
+ "subjectId": subject_id,
1796
+ "remoteId": remote_id,
1797
+ "tree": enriched_tree
1798
+ })
1799
+
1800
+
1801
+ @app.route("/api/path/generate-lesson", methods=["POST"])
1802
+ def generate_path_lesson():
1803
+ """
1804
+ Generates a revision lesson grounded in the specific syllabus point.
1805
+ Body: { subjectId, nodeId, nodeTitle }
1806
+ """
1807
+ uid = verify_token(request.headers.get("Authorization"))
1808
+ if not uid:
1809
+ return jsonify({"error": "Unauthorized"}), 401
1810
+
1811
+ data = request.get_json() or {}
1812
+ subject_id = data.get("subjectId")
1813
+ node_id = data.get("nodeId") # e.g. "A_9702_1.1"
1814
+ node_title = data.get("nodeTitle")
1815
+
1816
+ if not subject_id or not node_title:
1817
+ return jsonify({"error": "Missing fields"}), 400
1818
+
1819
+ remote_id = PATH_SUBJECT_MAP.get(subject_id)
1820
+
1821
+ # 1. Retrieve Context
1822
+ # We search specifically for the title to get the exact syllabus bullet points
1823
+ context_chunks = search_remote_context(node_title, remote_id)
1824
+ context_text = "\n".join([c.get("content", "") for c in context_chunks[:3]])
1825
+
1826
+ # 2. AI Generation
1827
+ prompt = f"""
1828
+ You are an expert tutor. Create a concise revision lesson.
1829
+
1830
+ Topic: {node_title}
1831
+ Syllabus Context (Strictly adhere to this scope):
1832
+ {context_text}
1833
+
1834
+ Structure:
1835
+ 1. **Concept Summary**: Simple explanation (2 paragraphs).
1836
+ 2. **Key Formulas/Definitions**: If applicable.
1837
+ 3. **Worked Example**: A typical exam-style problem with solution.
1838
+ 4. **Common Pitfalls**: What do students usually get wrong?
1839
+
1840
+ Format using Markdown.
1841
+ """
1842
+
1843
+ lesson_text = send_gemini_text(prompt)
1844
+
1845
+ return jsonify({
1846
+ "nodeId": node_id,
1847
+ "lessonContent": lesson_text
1848
+ })
1849
+
1850
+
1851
+ @app.route("/api/path/generate-quiz", methods=["POST"])
1852
+ def generate_path_quiz():
1853
+ """
1854
+ Generates a quiz to test mastery of a node.
1855
+ Body: { subjectId, nodeId, nodeTitle }
1856
+ """
1857
+ uid = verify_token(request.headers.get("Authorization"))
1858
+ if not uid:
1859
+ return jsonify({"error": "Unauthorized"}), 401
1860
+
1861
+ data = request.get_json() or {}
1862
+ subject_id = data.get("subjectId")
1863
+ node_title = data.get("nodeTitle")
1864
+ remote_id = PATH_SUBJECT_MAP.get(subject_id)
1865
+
1866
+ # 1. Retrieve Context
1867
+ context_chunks = search_remote_context(node_title, remote_id)
1868
+ context_text = "\n".join([c.get("content", "") for c in context_chunks[:3]])
1869
+
1870
+ # 2. AI Generation
1871
+ prompt = f"""
1872
+ Generate 5 Multiple Choice Questions to test: {node_title}
1873
+ Use this syllabus context as ground truth:
1874
+ {context_text}
1875
+
1876
+ Return JSON ONLY:
1877
+ {{
1878
+ "questions": [
1879
+ {{
1880
+ "stem": "Question text...",
1881
+ "options": ["A", "B", "C", "D"],
1882
+ "answer": "Correct Option Text"
1883
+ }}
1884
+ ]
1885
+ }}
1886
+ """
1887
+
1888
+ raw = send_gemini_text(prompt)
1889
+ try:
1890
+ clean = raw.replace("```json", "").replace("```", "").strip()
1891
+ quiz_data = json.loads(clean)
1892
+ except Exception as e:
1893
+ logger.error(f"Failed to parse quiz JSON: {raw}")
1894
+ return jsonify({"error": "Failed to generate quiz"}), 500
1895
+
1896
+ return jsonify(quiz_data)
1897
+
1898
+
1899
+ @app.route("/api/path/submit-node", methods=["POST"])
1900
+ def submit_path_node():
1901
+ """
1902
+ Records success on a node, unlocking the next step.
1903
+ Body: { subjectId, nodeId, score, total }
1904
+ """
1905
+ uid = verify_token(request.headers.get("Authorization"))
1906
+ if not uid:
1907
+ return jsonify({"error": "Unauthorized"}), 401
1908
+
1909
+ data = request.get_json() or {}
1910
+ subject_id = data.get("subjectId")
1911
+ node_id = data.get("nodeId")
1912
+ score = data.get("score", 0)
1913
+ total = data.get("total", 1)
1914
+
1915
+ if not subject_id or not node_id:
1916
+ return jsonify({"error": "Missing fields"}), 400
1917
+
1918
+ percentage = (score / total) * 100
1919
+ passed = percentage >= 70 # Pass threshold
1920
+
1921
+ if passed:
1922
+ # Mark as complete in Firebase
1923
+ # We record the timestamp of completion
1924
+ db_ref.child(f"users/{uid}/path_progress/{subject_id}/{node_id}").set(int(time.time()))
1925
+
1926
+ return jsonify({
1927
+ "success": True,
1928
+ "passed": True,
1929
+ "message": "Node completed! Next topic unlocked."
1930
+ })
1931
+ else:
1932
+ return jsonify({
1933
+ "success": True,
1934
+ "passed": False,
1935
+ "message": "Score too low. Review the lesson and try again."
1936
+ })
1937
  # -----------------------------------------------------------------------------
1938
+ # 12. MAIN
1939
  # -----------------------------------------------------------------------------
1940
 
1941
  if __name__ == "__main__":