Compare commits

..

No commits in common. "f11131df8d84ec7b4cc0b783f8ef5c5c99916c91" and "214276ebc0ae70327a8789e97b7b6331707ef19d" have entirely different histories.

3 changed files with 94 additions and 194 deletions

View File

@ -491,8 +491,6 @@ class RedisManager:
try: try:
if not self.client: if not self.client:
logger.info("Redis health check has no active client; connecting now")
if not self.connect():
raise Exception("No Redis connection") raise Exception("No Redis connection")
# Test connection # Test connection

View File

@ -166,66 +166,27 @@ def _query_month_days(month_uuid: str) -> List[Dict]:
return [] return []
def _query_teacher_classes( def _query_teacher_classes(institute_db: str, teacher_uuid: str) -> List[Dict]:
user_id: str, institute_id: str, institute_db: str, section_id: str = ""
) -> List[Dict]:
"""Query classes for a teacher or student from Supabase (source of truth)."""
try: try:
sb = _sb() with driver_tools.get_session(database=institute_db) as session:
teacher_rows = ( result = session.run(
sb.supabase.table("class_teachers") "MATCH (t:Teacher {uuid_string: $uuid})-[:TEACHER_HAS_CLASS]->(c:SubjectClass) "
.select("class_id, is_primary") "RETURN c ORDER BY c.name",
.eq("teacher_id", user_id) uuid=teacher_uuid,
.execute()
.data or []
) )
teacher_class_ids = {r["class_id"] for r in teacher_rows} return [
student_rows = ( {
sb.supabase.table("class_students") "neo4j_node_id": r["c"]["uuid_string"],
.select("class_id") "label": r["c"].get("name") or "Class",
.eq("student_id", user_id)
.eq("status", "active")
.execute()
.data or []
)
student_class_ids = {r["class_id"] for r in student_rows}
all_ids = list(teacher_class_ids | student_class_ids)
if not all_ids:
return []
classes = (
sb.supabase.table("classes")
.select("id, name, class_code, subject")
.in_("id", all_ids)
.eq("institute_id", institute_id)
.eq("is_active", True)
.order("name")
.execute()
.data or []
)
result = []
for c in classes:
role = "teacher" if c["id"] in teacher_class_ids else "student"
label = c.get("class_code") or c.get("name") or "Class"
node: Dict = {
"neo4j_node_id": c["id"],
"label": label,
"node_type": "SubjectClass", "node_type": "SubjectClass",
"neo4j_db_name": institute_db, "neo4j_db_name": institute_db,
"is_section": False, "is_section": False,
"has_children": True, "has_children": True,
"neo4j_props": {
"role": role,
"subject": c.get("subject") or "",
"name": c.get("name") or "",
"class_code": c.get("class_code") or "",
},
} }
if section_id: for r in result
node["section_id"] = section_id ]
result.append(node)
return result
except Exception as e: except Exception as e:
logger.warning(f"Could not query classes for user {user_id}: {e}") logger.warning(f"Could not query classes for teacher {teacher_uuid}: {e}")
return [] return []
@ -274,8 +235,8 @@ def _build_calendar_section() -> Dict:
return _section("calendar", "Calendar", "classroomcopilot", "empty") return _section("calendar", "Calendar", "classroomcopilot", "empty")
def _build_timetable_section(user_id: str, institute_id: Optional[str], institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict: def _build_timetable_section(institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict:
if not institute_db or not teacher_uuid or not institute_id: if not institute_db or not teacher_uuid:
return _section("timetable", "My Timetable", "", "no_school") return _section("timetable", "My Timetable", "", "no_school")
try: try:
@ -288,8 +249,27 @@ def _build_timetable_section(user_id: str, institute_id: Optional[str], institut
if rec: if rec:
tt = rec["tt"] tt = rec["tt"]
tt_uuid = tt["uuid_string"] tt_uuid = tt["uuid_string"]
# Load classes from Supabase (source of truth) classes = []
classes = _query_teacher_classes(user_id, institute_id, institute_db, section_id="timetable") try:
cls_result = session.run(
"MATCH (tt2:TeacherTimetable {uuid_string: $id})"
"-[:TIMETABLE_HAS_CLASS]->(c:SubjectClass) "
"RETURN c ORDER BY c.name",
id=tt_uuid,
)
classes = [
{
"neo4j_node_id": r["c"]["uuid_string"],
"label": r["c"].get("name") or "Class",
"node_type": "SubjectClass",
"neo4j_db_name": institute_db,
"is_section": False,
"has_children": True,
}
for r in cls_result
]
except Exception:
pass
return { return {
**_section("timetable", "My Timetable", institute_db, "populated", **_section("timetable", "My Timetable", institute_db, "populated",
has_children=True, children=classes if classes else None), has_children=True, children=classes if classes else None),
@ -303,11 +283,11 @@ def _build_timetable_section(user_id: str, institute_id: Optional[str], institut
return _section("timetable", "My Timetable", institute_db, "empty") return _section("timetable", "My Timetable", institute_db, "empty")
def _build_classes_section(user_id: str, institute_id: Optional[str], institute_db: Optional[str]) -> Dict: def _build_classes_section(institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict:
if not institute_db or not institute_id: if not institute_db or not teacher_uuid:
return _section("classes", "My Classes", "", "no_school") return _section("classes", "My Classes", "", "no_school")
classes = _query_teacher_classes(user_id, institute_id, institute_db, section_id="classes") classes = _query_teacher_classes(institute_db, teacher_uuid)
if classes: if classes:
return _section("classes", "My Classes", institute_db, "populated", return _section("classes", "My Classes", institute_db, "populated",
has_children=True, children=classes) has_children=True, children=classes)
@ -433,8 +413,27 @@ def _get_children_for_node(
# TeacherTimetable lazy-load (fallback if not pre-loaded, or for By-Term view) # TeacherTimetable lazy-load (fallback if not pre-loaded, or for By-Term view)
if node_type == "TeacherTimetable" and neo4j_db_name: if node_type == "TeacherTimetable" and neo4j_db_name:
if section_id in ("", "timetable"): if section_id in ("", "timetable"):
# Classes are pre-loaded in _build_timetable_section via Supabase. try:
# Lazy expansion here is not needed in normal flow. with driver_tools.get_session(database=neo4j_db_name) as session:
result = session.run(
"MATCH (tt:TeacherTimetable {uuid_string: $id})"
"-[:TIMETABLE_HAS_CLASS]->(c:SubjectClass) "
"RETURN c ORDER BY c.name",
id=neo4j_node_id,
)
return [
{
"neo4j_node_id": r["c"]["uuid_string"],
"label": r["c"].get("name") or "Class",
"node_type": "SubjectClass",
"neo4j_db_name": neo4j_db_name,
"is_section": False,
"has_children": True,
}
for r in result
]
except Exception as e:
logger.warning(f"TeacherTimetable class children failed: {e}")
return [] return []
if section_id == "timetable-term": if section_id == "timetable-term":
try: try:
@ -462,84 +461,6 @@ def _get_children_for_node(
logger.warning(f"TeacherTimetable term children failed: {e}") logger.warning(f"TeacherTimetable term children failed: {e}")
return [] return []
# SubjectClass — expand to taught lessons (timetable context) or members (classes context)
if node_type == "SubjectClass":
class_id = neo4j_node_id # Supabase class UUID
try:
sb = _sb()
if section_id == "timetable" and user_email:
# Resolve teacher profile_id from email
prof = sb.supabase.table("profiles").select("id").eq("email", user_email).single().execute()
teacher_id = (prof.data or {}).get("id")
if teacher_id:
lessons = (
sb.supabase.table("taught_lessons")
.select("id, date, period_code, class_name, subject")
.eq("class_id", class_id)
.eq("teacher_profile_id", teacher_id)
.order("date")
.order("period_code")
.execute()
.data or []
)
return [
{
"neo4j_node_id": tl["id"],
"label": "{} {}".format(
tl.get("period_code") or "",
tl.get("date") or ""
).strip(),
"node_type": "TaughtLesson",
"neo4j_db_name": neo4j_db_name,
"is_section": False,
"has_children": False,
"section_id": "timetable",
}
for tl in lessons
]
return []
if section_id == "classes":
# Class members: students enrolled in this class
members = (
sb.supabase.table("class_students")
.select("student_id, status, enrolled_at")
.eq("class_id", class_id)
.order("enrolled_at")
.execute()
.data or []
)
if not members:
return []
student_ids = [m["student_id"] for m in members]
status_map = {m["student_id"]: m["status"] for m in members}
profiles = (
sb.supabase.table("profiles")
.select("id, email, first_name, last_name, user_type")
.in_("id", student_ids)
.order("last_name")
.execute()
.data or []
)
return [
{
"neo4j_node_id": p["id"],
"label": "{} {} ({})".format(
p.get("first_name") or "",
p.get("last_name") or "",
status_map.get(p["id"], ""),
).strip(),
"node_type": "Student",
"neo4j_db_name": neo4j_db_name,
"is_section": False,
"has_children": False,
"section_id": "classes",
}
for p in profiles
]
except Exception as e:
logger.warning(f"SubjectClass children failed: {e}")
return []
# Section containers that need lazy loading # Section containers that need lazy loading
if node_type == "Section": if node_type == "Section":
if section_id == "timetable" and neo4j_db_name: if section_id == "timetable" and neo4j_db_name:
@ -645,52 +566,33 @@ def _get_children_for_node(
# AcademicWeek → days (or TaughtLessons in timetable-term context) # AcademicWeek → days (or TaughtLessons in timetable-term context)
if node_type == "AcademicWeek" and neo4j_db_name: if node_type == "AcademicWeek" and neo4j_db_name:
if section_id == "timetable-term" and user_email: if section_id == "timetable-term" and user_email:
# Supabase: get week date range from Neo4j, then query taught_lessons
try: try:
week_start = None
with driver_tools.get_session(database=neo4j_db_name) as session: with driver_tools.get_session(database=neo4j_db_name) as session:
rec = session.run( result = session.run(
"MATCH (w:AcademicWeek {uuid_string: $id}) " "MATCH (w:AcademicWeek {uuid_string: $id}) "
"RETURN w.start_date AS start_date", "-[:ACADEMIC_WEEK_HAS_ACADEMIC_DAY]->(d:AcademicDay) "
"-[:ACADEMIC_DAY_HAS_PERIOD]->(p:AcademicPeriod) "
"-[:ACADEMIC_PERIOD_HAS_TAUGHT_LESSON]->(tl:TaughtLesson) "
"WHERE tl.teacher_email = "
"RETURN tl, d.date AS date ORDER BY d.date, p.start_time",
id=neo4j_node_id, id=neo4j_node_id,
).single() email=user_email,
if rec:
week_start = rec["start_date"]
if week_start:
from datetime import datetime, timedelta
start_dt = datetime.strptime(str(week_start)[:10], "%Y-%m-%d")
end_dt = start_dt + timedelta(days=6)
sb = _sb()
prof = sb.supabase.table("profiles").select("id").eq("email", user_email).single().execute()
teacher_id = (prof.data or {}).get("id")
if teacher_id:
lessons = (
sb.supabase.table("taught_lessons")
.select("id, date, period_code, class_name, subject")
.eq("teacher_profile_id", teacher_id)
.gte("date", start_dt.strftime("%Y-%m-%d"))
.lte("date", end_dt.strftime("%Y-%m-%d"))
.order("date")
.order("period_code")
.execute()
.data or []
) )
return [ return [
{ {
"neo4j_node_id": tl["id"], "neo4j_node_id": r["tl"]["uuid_string"],
"label": "{}{}".format( "label": (r["tl"].get("period_code") or "")
tl.get("period_code") or "", + ""
tl.get("class_name") or tl.get("subject") or "Lesson" + (r["tl"].get("class_name") or r["tl"].get("subject_class") or "Lesson"),
),
"node_type": "TaughtLesson", "node_type": "TaughtLesson",
"neo4j_db_name": neo4j_db_name, "neo4j_db_name": neo4j_db_name,
"is_section": False, "is_section": False,
"has_children": False, "has_children": False,
} }
for tl in lessons for r in result
] ]
except Exception as e: except Exception as e:
logger.warning(f"AcademicWeek timetable-term Supabase lessons failed: {e}") logger.warning(f"AcademicWeek timetable-term lessons failed: {e}")
return [] return []
try: try:
with driver_tools.get_session(database=neo4j_db_name) as session: with driver_tools.get_session(database=neo4j_db_name) as session:
@ -789,12 +691,12 @@ async def get_teacher_graph_tree(
"has_children": True, "has_children": True,
} }
supabase_institute_id, institute_db, teacher_node_uuid = _resolve_institute(user_id, user_email) _, institute_db, teacher_node_uuid = _resolve_institute(user_id, user_email)
sections = [ sections = [
_build_calendar_section(), _build_calendar_section(),
_build_timetable_section(user_id, supabase_institute_id, institute_db, teacher_node_uuid), _build_timetable_section(institute_db, teacher_node_uuid),
_build_classes_section(user_id, supabase_institute_id, institute_db), _build_classes_section(institute_db, teacher_node_uuid),
_build_curriculum_section(institute_db), _build_curriculum_section(institute_db),
_build_journal_section(teacher_db), _build_journal_section(teacher_db),
_build_planner_section(teacher_db), _build_planner_section(teacher_db),

View File

@ -16,7 +16,7 @@ logging = logger.get_logger(
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from langchain_classic.chains import GraphCypherQAChain from langchain_classic.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph from langchain_community.graphs import Neo4jGraph
from langchain_openai import ChatOpenAI from langchain_community.chat_models import ChatOpenAI
from langchain_classic.prompts.prompt import PromptTemplate from langchain_classic.prompts.prompt import PromptTemplate
from routers.llm.private.ollama.ollama_wrapper import OllamaWrapper from routers.llm.private.ollama.ollama_wrapper import OllamaWrapper
from modules.database.tools.neontology.utils import get_node_types, get_rels_by_type from modules.database.tools.neontology.utils import get_node_types, get_rels_by_type