github-actions[bot] commited on
Commit
35426f1
ยท
1 Parent(s): bafd639

๐Ÿš€ Auto-deploy backend from GitHub (4d9c551)

Browse files
Files changed (2) hide show
  1. main.py +304 -49
  2. services/memory_service.py +123 -0
main.py CHANGED
@@ -131,6 +131,8 @@ try:
131
  update_active_topic,
132
  load_profile,
133
  finalize_session,
 
 
134
  )
135
  HAS_MEMORY_SERVICE = True
136
  except ImportError:
@@ -143,6 +145,8 @@ except ImportError:
143
  update_active_topic = None
144
  load_profile = None
145
  finalize_session = None
 
 
146
 
147
  try:
148
  import firebase_admin # type: ignore[import-not-found]
@@ -1521,61 +1525,238 @@ _NON_MATH_REDIRECT_RESPONSES: Tuple[str, ...] = (
1521
  )
1522
 
1523
  _MATH_SCOPE_KEYWORDS: Set[str] = {
1524
- "math",
1525
- "mathematics",
1526
- "algebra",
1527
  "geometry",
1528
- "trigonometry",
1529
  "calculus",
1530
- "statistics",
1531
  "probability",
1532
- "arithmetic",
1533
- "equation",
1534
- "inequality",
1535
- "function",
1536
- "graph",
1537
- "slope",
1538
- "derivative",
1539
- "integral",
1540
- "limit",
1541
- "matrix",
1542
- "determinant",
1543
- "fraction",
1544
- "percentage",
1545
- "ratio",
1546
- "polynomial",
1547
- "quadratic",
1548
- "logarithm",
1549
- "exponent",
1550
- "angle",
1551
- "triangle",
1552
- "circle",
1553
- "perimeter",
1554
- "area",
1555
- "volume",
1556
- "mean",
1557
- "median",
1558
- "mode",
1559
- "standard deviation",
1560
- "solve",
1561
- "simplify",
1562
- "factor",
1563
- "evaluate",
1564
- "compute",
1565
- "calculate",
1566
- "step by step",
1567
- "real life",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1568
  "example",
1569
- "why",
1570
- "how",
1571
- "remember",
 
 
 
 
 
 
 
1572
  }
1573
 
1574
  _MATH_SCOPE_PATTERNS: Tuple[re.Pattern[str], ...] = (
 
1575
  re.compile(r"\d+\s*[%+\-*/^=]\s*[-+]?\d*"),
1576
- re.compile(r"\b(?:sin|cos|tan|cot|sec|csc|log|ln|sqrt)\s*\(?"),
1577
  re.compile(r"\b(?:differentiate|integrate|derive|proof|prove)\b"),
1578
  re.compile(r"\b(?:x|y|z)\s*[=+\-*/^]\s*[-+]?\d"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1579
  )
1580
 
1581
  _CONTINUATION_FOLLOWUP_TOKENS: Set[str] = {
@@ -1604,15 +1785,70 @@ _CONTINUATION_CONTEXT_CLARIFY_RESPONSE = (
1604
  )
1605
 
1606
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1607
  def is_math_related_query(message: str) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
1608
  normalized = (message or "").strip().lower()
1609
  if not normalized:
1610
  return False
1611
 
1612
- if any(keyword in normalized for keyword in _MATH_SCOPE_KEYWORDS):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1613
  return True
1614
 
1615
- return any(pattern.search(normalized) for pattern in _MATH_SCOPE_PATTERNS)
 
 
 
 
 
1616
 
1617
 
1618
  def _normalize_continuation_followup_token(message: str) -> str:
@@ -1958,6 +2194,18 @@ async def health_check():
1958
  }
1959
 
1960
 
 
 
 
 
 
 
 
 
 
 
 
 
1961
  @app.get("/debug/scope-info")
1962
  async def debug_scope_info():
1963
  """Reveal what scope check code is deployed and its current state."""
@@ -1969,8 +2217,8 @@ async def debug_scope_info():
1969
  return {
1970
  "git_commit": git_hash,
1971
  "math_keywords": list(_MATH_SCOPE_KEYWORDS),
1972
- "pattern_names": list(_MATH_SCOPE_PATTERNS.keys()),
1973
- "has_history_check": "_has_math_context_in_history" in globals() or "_has_math_context_in_history" in dir(),
1974
  "thanks_pattern": _THANKS_PATTERN.pattern if hasattr(_THANKS_PATTERN, 'pattern') else str(_THANKS_PATTERN),
1975
  }
1976
 
@@ -2245,6 +2493,7 @@ def _build_stream_continuation_prompt(original_question: str, expected_end_marke
2245
  @app.post("/api/chat", response_model=ChatResponse)
2246
  async def chat_tutor(request: ChatRequest):
2247
  """AI Math Tutor powered by Hugging Face Inference routing."""
 
2248
  try:
2249
  boundary_response = get_scope_boundary_response(request.message, request.history)
2250
  if boundary_response is not None:
@@ -2253,6 +2502,7 @@ async def chat_tutor(request: ChatRequest):
2253
  system_prompt = MATH_TUTOR_SYSTEM_PROMPT
2254
 
2255
  # โ”€โ”€โ”€ Memory Context Injection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
2256
  memory_context = ""
2257
  _mem = collect_memory_context # local alias for type safety
2258
  if request.userId and request.sessionId and _mem is not None:
@@ -2266,6 +2516,7 @@ async def chat_tutor(request: ChatRequest):
2266
  logger.debug(f"Memory context injection skipped: {mem_err}")
2267
  if memory_context:
2268
  system_prompt = memory_context + "\n\n" + system_prompt
 
2269
  # โ”€โ”€โ”€ End Memory Context โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2270
 
2271
  if request.userId and HAS_FIREBASE_ADMIN and firebase_firestore:
@@ -2315,6 +2566,7 @@ Overall Risk Level: {risk.get('overall_risk', 'unknown')}
2315
  messages.append({"role": "user", "content": request.message})
2316
 
2317
  # Call HF serverless with retry (handled inside call_hf_chat)
 
2318
  try:
2319
  answer = await call_hf_chat_async(
2320
  messages,
@@ -2329,6 +2581,7 @@ Overall Risk Level: {risk.get('overall_risk', 'unknown')}
2329
  status_code=502,
2330
  detail="AI model service is temporarily unavailable. Please try again.",
2331
  )
 
2332
 
2333
  # โ”€โ”€โ”€ Background Memory Update (async, non-blocking) โ”€โ”€โ”€โ”€โ”€
2334
  if request.userId and request.sessionId and HAS_MEMORY_SERVICE:
@@ -2349,6 +2602,7 @@ Overall Risk Level: {risk.get('overall_risk', 'unknown')}
2349
  if request.verify:
2350
  logger.info("Running self-consistency verification for chat response")
2351
  verification = await verify_math_response(request.message, messages)
 
2352
  return ChatResponse(
2353
  response=verification["response"],
2354
  verified=verification["verified"],
@@ -2356,6 +2610,7 @@ Overall Risk Level: {risk.get('overall_risk', 'unknown')}
2356
  warning=verification.get("warning"),
2357
  )
2358
 
 
2359
  return ChatResponse(response=answer)
2360
 
2361
  except HTTPException:
 
131
  update_active_topic,
132
  load_profile,
133
  finalize_session,
134
+ check_memory_health,
135
+ log_timing,
136
  )
137
  HAS_MEMORY_SERVICE = True
138
  except ImportError:
 
145
  update_active_topic = None
146
  load_profile = None
147
  finalize_session = None
148
+ check_memory_health = None
149
+ log_timing = None
150
 
151
  try:
152
  import firebase_admin # type: ignore[import-not-found]
 
1525
  )
1526
 
1527
  _MATH_SCOPE_KEYWORDS: Set[str] = {
1528
+ # Core math subjects
1529
+ "math", "mathematics",
1530
+ "algebra", "arithmetic",
1531
  "geometry",
1532
+ "trigonometry", "trig",
1533
  "calculus",
1534
+ "statistics", "statistic",
1535
  "probability",
1536
+ "precalculus", "pre-calculus",
1537
+ "discrete math", "discrete mathematics",
1538
+ "linear algebra",
1539
+ "number theory",
1540
+ "set theory",
1541
+ # Geometry terms
1542
+ "triangle", "triangles", "polygon", "polygons",
1543
+ "circle", "circles", "sphere", "spheres",
1544
+ "angle", "angles", "right angle", "acute angle", "obtuse angle",
1545
+ "hypotenuse", "pythagorean", "pythagoras",
1546
+ "theorem", "theorems",
1547
+ "proof", "prove",
1548
+ "congruent", "similar", "similarity",
1549
+ "parallel", "perpendicular",
1550
+ "diameter", "radius", "circumference",
1551
+ "perimeter", "area", "volume",
1552
+ "surface area", "surfacearea",
1553
+ "prism", "cylinder", "cone", "pyramid",
1554
+ "coordinate", "coordinates", "coordinate plane",
1555
+ "transform", "transformation",
1556
+ "rotation", "reflection", "translation", "dilation",
1557
+ "scale factor",
1558
+ "line segment", "ray", "midpoint", "bisector",
1559
+ # Algebra terms
1560
+ "equation", "equations",
1561
+ "variable", "variables",
1562
+ "expression", "expressions",
1563
+ "expand", "expansion",
1564
+ "linear", "linear equation",
1565
+ "system of equations", "systems of equations",
1566
+ "coefficient", "coefficients", "constant",
1567
+ "binomial", "trinomial",
1568
+ "rational", "rational expression",
1569
+ "radical", "radicals", "square root",
1570
+ "absolute value",
1571
+ "sequence", "sequences",
1572
+ "series", "summation",
1573
+ "recursive", "explicit",
1574
+ "domain", "range",
1575
+ "asymptote", "asymptotes",
1576
+ "intercept", "x-intercept", "y-intercept",
1577
+ "vertex", "vertices",
1578
+ "parabola", "ellipse", "hyperbola",
1579
+ "inequality", "inequalities",
1580
+ "polynomial", "polynomials",
1581
+ "quadratic", "quadratics",
1582
+ "function", "functions",
1583
+ "graph", "graphs", "graphing",
1584
+ "slope", "slopes",
1585
+ "logarithm", "logarithms",
1586
+ "exponent", "exponents",
1587
+ "exponential", "logarithmic",
1588
+ "fraction", "fractions",
1589
+ "percentage", "percentages",
1590
+ "ratio", "ratios", "proportion",
1591
+ "solve", "solving", "solution",
1592
+ "simplify", "simplification",
1593
+ "factor", "factoring", "factorization",
1594
+ "evaluate", "evaluating",
1595
+ "compute", "computing",
1596
+ "calculate", "calculation",
1597
+ "substitute", "substitution",
1598
+ "isolate", "manipulate",
1599
+ # Calculus terms
1600
+ "derivative", "derivatives",
1601
+ "differentiate", "differentiation",
1602
+ "integral", "integrals",
1603
+ "integrate", "integration",
1604
+ "antiderivative",
1605
+ "limit", "limits",
1606
+ "rate of change",
1607
+ "differential",
1608
+ "indefinite", "definite",
1609
+ "area under", "are aunder",
1610
+ "curve", "curves",
1611
+ "tangent", "tangent line",
1612
+ "normal line",
1613
+ "optimization",
1614
+ "related rates",
1615
+ "implicit", "implicit differentiation",
1616
+ "partial derivative", "partial derivatives",
1617
+ "continuity", "continuous",
1618
+ "discontinuity",
1619
+ "converge", "convergence",
1620
+ "diverge", "divergence",
1621
+ "improper integral",
1622
+ "series", "power series",
1623
+ "taylor", "maclaurin",
1624
+ # Trigonometry terms
1625
+ "sine", "sin", "sinusoidal",
1626
+ "cosine", "cos",
1627
+ "tangent", "tan",
1628
+ "cosecant", "csc",
1629
+ "secant", "sec",
1630
+ "cotangent", "cot",
1631
+ "radian", "radians",
1632
+ "degree", "degrees",
1633
+ "unit circle",
1634
+ "period", "periodic",
1635
+ "amplitude",
1636
+ "phase shift", "phase shift",
1637
+ "frequency",
1638
+ "identity", "identities",
1639
+ "reciprocal",
1640
+ "half-angle", "double-angle",
1641
+ "sum and difference",
1642
+ "law of sines",
1643
+ "law of cosines",
1644
+ "inverse trig", "inverse trigonometric",
1645
+ "arcsin", "arccos", "arctan",
1646
+ "trigonometric",
1647
+ # Statistics & probability terms
1648
+ "data", "dataset",
1649
+ "distribution", "distributions",
1650
+ "sample", "sampling",
1651
+ "population",
1652
+ "variance",
1653
+ "standard deviation", "stdev",
1654
+ "correlation",
1655
+ "regression",
1656
+ "z-score", "zscore",
1657
+ "t-test", "chi-square", "chisquare",
1658
+ "confidence interval",
1659
+ "hypothesis", "hypotheses",
1660
+ "null hypothesis",
1661
+ "alternative hypothesis",
1662
+ "p-value", "pvalue",
1663
+ "significance", "significance level",
1664
+ "normal distribution",
1665
+ "binomial", "binomial distribution",
1666
+ "poisson", "poisson distribution",
1667
+ "uniform distribution",
1668
+ "random", "randomly",
1669
+ "random variable",
1670
+ "expected value",
1671
+ "outcome", "outcomes",
1672
+ "event", "events",
1673
+ "conditional", "conditional probability",
1674
+ "bayes", "bayesian",
1675
+ "permutation", "permutations",
1676
+ "combination", "combinations",
1677
+ "frequency", "frequencies",
1678
+ "histogram", "histograms",
1679
+ "box plot", "boxplot",
1680
+ "scatter plot", "scatterplot",
1681
+ "quartile", "quartiles",
1682
+ "interquartile",
1683
+ "outlier", "outliers",
1684
+ "margin of error",
1685
+ "census", "survey",
1686
+ "mean", "median", "mode",
1687
+ "average",
1688
+ "weighted",
1689
+ # Natural language framing
1690
+ "explain", "explanation",
1691
+ "what is", "what's", "whats",
1692
+ "what are", "what do", "what does",
1693
+ "how does", "how is", "how do",
1694
+ "why do", "why does", "why is",
1695
+ "where is", "where are",
1696
+ "describe", "description",
1697
+ "tell me about",
1698
+ "can you explain",
1699
+ "define", "definition",
1700
+ "meaning of", "meaning",
1701
+ "difference between",
1702
+ "relationship between",
1703
+ "compare", "contrast",
1704
+ "who discovered", "who invented",
1705
+ "history of",
1706
+ "origin of",
1707
+ "list",
1708
+ # Real-world application signals
1709
+ "real life", "real-life", "reallife",
1710
+ "used in", "used for",
1711
+ "application", "applications",
1712
+ "use case", "use cases",
1713
+ "practical", "practically",
1714
+ "everyday", "daily life",
1715
+ "when would", "where can",
1716
+ "step by step", "step-by-step",
1717
  "example",
1718
+ "problem solving",
1719
+ "word problem", "word problems",
1720
+ "how to",
1721
+ # SHS Math topics
1722
+ "stem", "stem math",
1723
+ "business math",
1724
+ "general math",
1725
+ "basic calculus",
1726
+ "pre-calculus",
1727
+ "probability and statistics",
1728
  }
1729
 
1730
  _MATH_SCOPE_PATTERNS: Tuple[re.Pattern[str], ...] = (
1731
+ # Formula/expression patterns
1732
  re.compile(r"\d+\s*[%+\-*/^=]\s*[-+]?\d*"),
1733
+ re.compile(r"\b(?:sin|cos|tan|cot|sec|csc|log|ln|sqrt)\s*\("),
1734
  re.compile(r"\b(?:differentiate|integrate|derive|proof|prove)\b"),
1735
  re.compile(r"\b(?:x|y|z)\s*[=+\-*/^]\s*[-+]?\d"),
1736
+ re.compile(r"\b[xXyYzZ]\s*\^"),
1737
+ # Theorem/concept names
1738
+ re.compile(r"\b(?:pythagorean|pythagoras|pythagorean theorem)\b", re.IGNORECASE),
1739
+ re.compile(r"\b(?:theorem|lemma|corollary|axiom|postulate)\b", re.IGNORECASE),
1740
+ # Natural language framing
1741
+ re.compile(r"^(?:explain|describe|tell me about|what is|what are|what's|whats|how does|how is|why do|why does|why is|can you explain|define|what does)\b", re.IGNORECASE),
1742
+ # Application signals
1743
+ re.compile(r"\b(?:used in|used for|application|real life|real-world|reallife|practical|use case)\b", re.IGNORECASE),
1744
+ # Trig functions as words
1745
+ re.compile(r"\b(?:sine|cosine|tangent|cosecant|secant|cotangent|sinusoidal|arcsin|arccos|arctan)\b", re.IGNORECASE),
1746
+ # Calculus operations
1747
+ re.compile(r"\b(?:differentiate|differentiation|integrate|integration|derivative|antiderivative|differentiation?|integration?)\b", re.IGNORECASE),
1748
+ # Stats terms
1749
+ re.compile(r"\b(?:mean|median|mode|variance|standard\s*deviation|correlation|regression|probability|distribution|hypothesis)\b", re.IGNORECASE),
1750
+ # Algebra/geometry operations
1751
+ re.compile(r"\b(?:solve\s+for|simplify|factor|expand|evaluate|compute|calculate)\b", re.IGNORECASE),
1752
+ # Geometry properties
1753
+ re.compile(r"\b(?:area of|perimeter of|volume of|surface area|circumference|diameter|radius)\b", re.IGNORECASE),
1754
+ # Comparison/contrast
1755
+ re.compile(r"\b(?:difference between|relationship between|compare|contrast)\s+(?:\w+\s+){0,3}(?:and|vs|versus|with)", re.IGNORECASE),
1756
+ # Learning/understanding signals
1757
+ re.compile(r"\b(?:i don't understand|i don't get|i'm confused|help me|can you help|struggle|confus|difficult|hard to)\b", re.IGNORECASE),
1758
+ # Proof derivation
1759
+ re.compile(r"\b(?:proof|prove|derivation|derive|show that)\b", re.IGNORECASE),
1760
  )
1761
 
1762
  _CONTINUATION_FOLLOWUP_TOKENS: Set[str] = {
 
1785
  )
1786
 
1787
 
1788
+ def _is_remember_storage_command(message: str) -> bool:
1789
+ """Detect 'remember' as a storage/note-taking command, not math context.
1790
+
1791
+ Matches patterns like:
1792
+ - "Remember: my favorite number is 42"
1793
+ - "Remember that I like..."
1794
+ - "Remember my name is..."
1795
+ - "Remember: ..."
1796
+
1797
+ These should NOT be treated as math context.
1798
+ """
1799
+ normalized = (message or "").strip().lower()
1800
+ # "remember" at the start followed by colon, "that", "my", "to", or "this"
1801
+ if re.match(r"remember\s*[:]\s", normalized) or \
1802
+ re.match(r"remember\s+that\s", normalized) or \
1803
+ re.match(r"remember\s+my\s", normalized) or \
1804
+ re.match(r"remember\s+this\s", normalized) or \
1805
+ re.match(r"remember\s+to\s", normalized) or \
1806
+ re.match(r"^remember\s", normalized):
1807
+ return True
1808
+ return False
1809
+
1810
+
1811
  def is_math_related_query(message: str) -> bool:
1812
+ """Determines if the message is related to math using a scoring approach.
1813
+
1814
+ Returns True if any math signal is found, False otherwise.
1815
+
1816
+ Uses scoring where:
1817
+ - Each keyword match = 1 point
1818
+ - Each regex pattern match = 2 points
1819
+ - Score >= 1 = math-related
1820
+ - Even a single clear signal is sufficient to pass through.
1821
+
1822
+ Exception: 'remember' storage commands are always rejected.
1823
+ """
1824
  normalized = (message or "").strip().lower()
1825
  if not normalized:
1826
  return False
1827
 
1828
+ # Special case: "remember" storage commands are never math
1829
+ if _is_remember_storage_command(message):
1830
+ return False
1831
+
1832
+ score = 0
1833
+
1834
+ # Score from keyword matches
1835
+ for keyword in _MATH_SCOPE_KEYWORDS:
1836
+ if keyword in normalized:
1837
+ # Short keywords (< 4 chars) need word boundary check to avoid false positives
1838
+ if len(keyword) >= 4 or keyword == "sin" or keyword == "cos" or keyword == "tan":
1839
+ score += 1
1840
+ if score >= 1:
1841
+ return True
1842
+
1843
+ if score >= 1:
1844
  return True
1845
 
1846
+ # Score from regex pattern matches
1847
+ for pattern in _MATH_SCOPE_PATTERNS:
1848
+ if pattern.search(normalized):
1849
+ return True
1850
+
1851
+ return False
1852
 
1853
 
1854
  def _normalize_continuation_followup_token(message: str) -> str:
 
2194
  }
2195
 
2196
 
2197
+ @app.get("/api/memory/health")
2198
+ async def memory_health_check(uid: str = ""):
2199
+ """Verify that all memory stores are writable and readable for a given uid."""
2200
+ if not HAS_MEMORY_SERVICE or check_memory_health is None:
2201
+ return {
2202
+ "available": False,
2203
+ "error": "Memory service not available",
2204
+ }
2205
+ result = check_memory_health(uid or "test_health_check_user")
2206
+ return result
2207
+
2208
+
2209
  @app.get("/debug/scope-info")
2210
  async def debug_scope_info():
2211
  """Reveal what scope check code is deployed and its current state."""
 
2217
  return {
2218
  "git_commit": git_hash,
2219
  "math_keywords": list(_MATH_SCOPE_KEYWORDS),
2220
+ "pattern_count": len(_MATH_SCOPE_PATTERNS),
2221
+ "has_history_check": "_has_math_context_in_history" in dir(),
2222
  "thanks_pattern": _THANKS_PATTERN.pattern if hasattr(_THANKS_PATTERN, 'pattern') else str(_THANKS_PATTERN),
2223
  }
2224
 
 
2493
  @app.post("/api/chat", response_model=ChatResponse)
2494
  async def chat_tutor(request: ChatRequest):
2495
  """AI Math Tutor powered by Hugging Face Inference routing."""
2496
+ _start_ms = int(time.monotonic() * 1000)
2497
  try:
2498
  boundary_response = get_scope_boundary_response(request.message, request.history)
2499
  if boundary_response is not None:
 
2502
  system_prompt = MATH_TUTOR_SYSTEM_PROMPT
2503
 
2504
  # โ”€โ”€โ”€ Memory Context Injection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2505
+ _t0 = int(time.monotonic() * 1000)
2506
  memory_context = ""
2507
  _mem = collect_memory_context # local alias for type safety
2508
  if request.userId and request.sessionId and _mem is not None:
 
2516
  logger.debug(f"Memory context injection skipped: {mem_err}")
2517
  if memory_context:
2518
  system_prompt = memory_context + "\n\n" + system_prompt
2519
+ logger.info(f"TIMING [memory_context_injection] {int(time.monotonic() * 1000) - _t0}ms")
2520
  # โ”€โ”€โ”€ End Memory Context โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2521
 
2522
  if request.userId and HAS_FIREBASE_ADMIN and firebase_firestore:
 
2566
  messages.append({"role": "user", "content": request.message})
2567
 
2568
  # Call HF serverless with retry (handled inside call_hf_chat)
2569
+ _t1 = int(time.monotonic() * 1000)
2570
  try:
2571
  answer = await call_hf_chat_async(
2572
  messages,
 
2581
  status_code=502,
2582
  detail="AI model service is temporarily unavailable. Please try again.",
2583
  )
2584
+ logger.info(f"TIMING [model_call] {int(time.monotonic() * 1000) - _t1}ms | messages={len(messages)}")
2585
 
2586
  # โ”€โ”€โ”€ Background Memory Update (async, non-blocking) โ”€โ”€โ”€โ”€โ”€
2587
  if request.userId and request.sessionId and HAS_MEMORY_SERVICE:
 
2602
  if request.verify:
2603
  logger.info("Running self-consistency verification for chat response")
2604
  verification = await verify_math_response(request.message, messages)
2605
+ logger.info(f"TIMING [chat_total] {int(time.monotonic() * 1000) - _start_ms}ms | verify=true")
2606
  return ChatResponse(
2607
  response=verification["response"],
2608
  verified=verification["verified"],
 
2610
  warning=verification.get("warning"),
2611
  )
2612
 
2613
+ logger.info(f"TIMING [chat_total] {int(time.monotonic() * 1000) - _start_ms}ms | verify=false")
2614
  return ChatResponse(response=answer)
2615
 
2616
  except HTTPException:
services/memory_service.py CHANGED
@@ -967,3 +967,126 @@ def clear_session(uid: str, session_id: str) -> None:
967
  def is_memory_available() -> bool:
968
  """Check if memory system is operational."""
969
  return _has_firestore()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  def is_memory_available() -> bool:
968
  """Check if memory system is operational."""
969
  return _has_firestore()
970
+
971
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
972
+ # 9. TIMING & HEALTH MONITORING
973
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
974
+
975
+ _TIMING_LOG_ENABLED: bool = True
976
+
977
+
978
+ def _time_ms() -> int:
979
+ """Return current monotonic time in milliseconds."""
980
+ return int(time.monotonic() * 1000)
981
+
982
+
983
+ def log_timing(func_name: str, start_ms: int) -> None:
984
+ """Log the elapsed time of a function call if timing is enabled."""
985
+ if not _TIMING_LOG_ENABLED:
986
+ return
987
+ elapsed = _time_ms() - start_ms
988
+ logger.info(f"TIMING [{func_name}] {elapsed}ms")
989
+
990
+
991
+ def check_memory_health(uid: str) -> dict:
992
+ """Verify that all three memory stores are writable and readable.
993
+
994
+ Returns a dict with per-store results:
995
+ {
996
+ 'firestore_available': bool,
997
+ 'profile_writable': {'ok': bool, 'latency_ms': int, 'error': str | None},
998
+ 'active_state_writable': {...},
999
+ 'session_summary_writable': {...},
1000
+ }
1001
+ """
1002
+ now = _time_ms()
1003
+ result: dict = {
1004
+ "firestore_available": _has_firestore(),
1005
+ "profile_writable": {"ok": False, "latency_ms": 0, "error": None},
1006
+ "active_state_writable": {"ok": False, "latency_ms": 0, "error": None},
1007
+ "session_summary_writable": {"ok": False, "latency_ms": 0, "error": None},
1008
+ }
1009
+
1010
+ if not result["firestore_available"]:
1011
+ for key in ("profile_writable", "active_state_writable", "session_summary_writable"):
1012
+ result[key]["error"] = "Firestore not initialized"
1013
+ result["_elapsed_ms"] = _time_ms() - now
1014
+ return result
1015
+
1016
+ db = firebase_admin.firestore.client()
1017
+ test_prefix = f"_health_check_test_{int(time.time())}"
1018
+ profile_ref = db.collection("users").document(uid).collection("tutorMemory").document("profile")
1019
+ active_ref = db.collection("users").document(uid).collection("tutorMemory").document("working").collection("state").document("active_state")
1020
+ session_ref = (
1021
+ db.collection("users")
1022
+ .document(uid)
1023
+ .collection("tutorMemory")
1024
+ .document("sessions")
1025
+ .collection("items")
1026
+ .document(f"{test_prefix}")
1027
+ )
1028
+
1029
+ # Test profile write
1030
+ try:
1031
+ t0 = _time_ms()
1032
+ profile_ref.set({"stable_facts": {"test_fact": "health_check_ok"}}, merge=True)
1033
+ readback = profile_ref.get()
1034
+ facts = readback.to_dict().get("stable_facts", {}) if readback.exists else {}
1035
+ latency = _time_ms() - t0
1036
+ if facts.get("test_fact") == "health_check_ok":
1037
+ result["profile_writable"]["ok"] = True
1038
+ else:
1039
+ result["profile_writable"]["error"] = "Write verification failed"
1040
+ result["profile_writable"]["latency_ms"] = latency
1041
+ # Cleanup
1042
+ profile_ref.update({"stable_facts.test_fact": firestore.DELETE_FIELD})
1043
+ except Exception as e:
1044
+ result["profile_writable"]["error"] = str(e)
1045
+ result["profile_writable"]["latency_ms"] = _time_ms() - t0
1046
+
1047
+ # Test active state write
1048
+ try:
1049
+ t0 = _time_ms()
1050
+ active_ref.set({"active_topic": "health_check_test", "turn_count": 0}, merge=True)
1051
+ readback = active_ref.get()
1052
+ data = readback.to_dict() if readback.exists else {}
1053
+ latency = _time_ms() - t0
1054
+ if data.get("active_topic") == "health_check_test":
1055
+ result["active_state_writable"]["ok"] = True
1056
+ else:
1057
+ result["active_state_writable"]["error"] = "Write verification failed"
1058
+ result["active_state_writable"]["latency_ms"] = latency
1059
+ # Cleanup
1060
+ active_ref.update({"active_topic": firestore.DELETE_FIELD, "turn_count": firestore.DELETE_FIELD})
1061
+ except Exception as e:
1062
+ result["active_state_writable"]["error"] = str(e)
1063
+ result["active_state_writable"]["latency_ms"] = _time_ms() - t0
1064
+
1065
+ # Test session summary write
1066
+ try:
1067
+ t0 = _time_ms()
1068
+ session_ref.set({
1069
+ "concepts_covered": ["health_check_test"],
1070
+ "key_insights": "health check",
1071
+ "timestamp": firestore.SERVER_TIMESTAMP,
1072
+ })
1073
+ readback = session_ref.get()
1074
+ data = readback.to_dict() if readback.exists else {}
1075
+ latency = _time_ms() - t0
1076
+ if data.get("key_insights") == "health check":
1077
+ result["session_summary_writable"]["ok"] = True
1078
+ else:
1079
+ result["session_summary_writable"]["error"] = "Write verification failed"
1080
+ result["session_summary_writable"]["latency_ms"] = latency
1081
+ # Cleanup
1082
+ session_ref.delete()
1083
+ except Exception as e:
1084
+ result["session_summary_writable"]["error"] = str(e)
1085
+ result["session_summary_writable"]["latency_ms"] = _time_ms() - t0
1086
+
1087
+ result["_elapsed_ms"] = _time_ms() - now
1088
+ logger.info(f"MEMORY_HEALTH check for uid={uid}: profile={result['profile_writable']['ok']}, "
1089
+ f"active={result['active_state_writable']['ok']}, "
1090
+ f"session={result['session_summary_writable']['ok']}, "
1091
+ f"elapsed={result['_elapsed_ms']}ms")
1092
+ return result