Spaces:
Running
Running
github-actions[bot] commited on
Commit ยท
35426f1
1
Parent(s): bafd639
๐ Auto-deploy backend from GitHub (4d9c551)
Browse files- main.py +304 -49
- 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 |
-
|
| 1525 |
-
"mathematics",
|
| 1526 |
-
"algebra",
|
| 1527 |
"geometry",
|
| 1528 |
-
"trigonometry",
|
| 1529 |
"calculus",
|
| 1530 |
-
"statistics",
|
| 1531 |
"probability",
|
| 1532 |
-
"
|
| 1533 |
-
"
|
| 1534 |
-
"
|
| 1535 |
-
"
|
| 1536 |
-
"
|
| 1537 |
-
|
| 1538 |
-
"
|
| 1539 |
-
"
|
| 1540 |
-
"
|
| 1541 |
-
"
|
| 1542 |
-
"
|
| 1543 |
-
"
|
| 1544 |
-
"
|
| 1545 |
-
"
|
| 1546 |
-
"
|
| 1547 |
-
"
|
| 1548 |
-
"
|
| 1549 |
-
"
|
| 1550 |
-
"
|
| 1551 |
-
"
|
| 1552 |
-
"
|
| 1553 |
-
"
|
| 1554 |
-
"
|
| 1555 |
-
|
| 1556 |
-
"
|
| 1557 |
-
"
|
| 1558 |
-
"
|
| 1559 |
-
"
|
| 1560 |
-
"
|
| 1561 |
-
"
|
| 1562 |
-
"
|
| 1563 |
-
"
|
| 1564 |
-
"
|
| 1565 |
-
"
|
| 1566 |
-
"
|
| 1567 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1568 |
"example",
|
| 1569 |
-
"
|
| 1570 |
-
"
|
| 1571 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1613 |
return True
|
| 1614 |
|
| 1615 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 1973 |
-
"has_history_check": "_has_math_context_in_history" in
|
| 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
|