asnannp commited on
Commit
5801154
Β·
1 Parent(s): 0ccfbfd

deploy: sync backend (real general answers, casual short-circuit, classifier, chat resilience)

Browse files
backend/app/services/learn_lesson_builder.py CHANGED
@@ -36,10 +36,11 @@ from app.core.config import BACKEND_DIR, PROJECT_ROOT, get_settings
36
 
37
  logger = logging.getLogger(__name__)
38
 
39
- # Writable cache for lesson manifests + audio. On Hugging Face Docker the
40
- # backend tree is /app, so PROJECT_ROOT/public is outside the container and
41
- # not writable (PermissionError β†’ opaque 500). Cache under BACKEND_DIR.
42
- # Tests may monkeypatch this path.
 
43
  PUBLIC_ROOT = BACKEND_DIR / "generated" / "learn-anything"
44
 
45
  GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
@@ -761,25 +762,49 @@ def _lesson_lock_for(key: str) -> threading.Lock:
761
 
762
 
763
  def ensure_public_root() -> Path:
764
- """Ensure the lesson cache directory exists and is writable."""
 
765
  env = _env("LEARN_LESSON_CACHE_DIR")
766
- root = Path(env) if env else PUBLIC_ROOT
767
- try:
768
- root.mkdir(parents=True, exist_ok=True)
769
- probe = root / ".write_probe"
770
- probe.write_text("ok", encoding="utf-8")
771
- probe.unlink(missing_ok=True)
772
- return root
773
- except OSError:
774
- fallback = Path(os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp") / "docdoe-learn-lessons"
 
 
 
 
 
 
 
 
 
775
  try:
776
- fallback.mkdir(parents=True, exist_ok=True)
777
- logger.warning("Lesson cache fell back to %s", fallback)
778
- return fallback
 
 
 
 
 
 
 
 
 
 
 
779
  except OSError as exc:
780
- raise LessonBuildError(
781
- f"Lesson cache directory is not writable ({root}): {exc}"
782
- ) from exc
 
 
 
783
 
784
 
785
  def build_lesson(
@@ -835,9 +860,26 @@ def build_lesson(
835
  try:
836
  out_dir.mkdir(parents=True, exist_ok=True)
837
  except OSError as exc:
838
- raise LessonBuildError(
839
- f"Could not create lesson cache folder ({out_dir}): {exc}"
840
- ) from exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
841
  delivery_mode = "reading" if mock_mode else "audio"
842
  delivery_notice = (
843
  "Local preview: this complete starter class is available in reading mode; no generated voice is being presented as real audio."
 
36
 
37
  logger = logging.getLogger(__name__)
38
 
39
+ # Writable cache for lesson manifests + audio.
40
+ # NEVER use PROJECT_ROOT/"public" β€” on HF Docker WORKDIR=/app, that resolves to
41
+ # /public which is not creatable (Permission denied β†’ 500).
42
+ # Prefer: LEARN_LESSON_CACHE_DIR β†’ /app/generated/... β†’ backend/generated/...
43
+ # Tests may monkeypatch PUBLIC_ROOT.
44
  PUBLIC_ROOT = BACKEND_DIR / "generated" / "learn-anything"
45
 
46
  GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
 
762
 
763
 
764
  def ensure_public_root() -> Path:
765
+ """Ensure the lesson cache directory exists and is writable on HF + local."""
766
+ candidates: list[Path] = []
767
  env = _env("LEARN_LESSON_CACHE_DIR")
768
+ if env:
769
+ candidates.append(Path(env))
770
+ # HF Docker WORKDIR is /app (backend tree).
771
+ candidates.append(Path("/app/generated/learn-anything"))
772
+ # Monorepo / local backend package root.
773
+ candidates.append(BACKEND_DIR / "generated" / "learn-anything")
774
+ # Tests monkeypatch PUBLIC_ROOT β€” honour it if already set to a temp path.
775
+ if PUBLIC_ROOT not in candidates:
776
+ candidates.insert(0, PUBLIC_ROOT)
777
+ # Last-resort temp (always writable for reading-mode fallbacks).
778
+ candidates.append(
779
+ Path(os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp")
780
+ / "docdoe-learn-lessons"
781
+ )
782
+
783
+ errors: list[str] = []
784
+ for root in candidates:
785
+ # Never attempt to create absolute /public (HF permission bomb).
786
  try:
787
+ resolved = root.resolve()
788
+ except OSError:
789
+ resolved = root
790
+ if str(resolved).startswith("/public") or str(resolved) == "/public":
791
+ errors.append(f"skip forbidden path {root}")
792
+ continue
793
+ try:
794
+ root.mkdir(parents=True, exist_ok=True)
795
+ probe = root / ".write_probe"
796
+ probe.write_text("ok", encoding="utf-8")
797
+ probe.unlink(missing_ok=True)
798
+ if root != PUBLIC_ROOT:
799
+ logger.info("Lesson cache using %s", root)
800
+ return root
801
  except OSError as exc:
802
+ errors.append(f"{root}: {exc}")
803
+ continue
804
+
805
+ raise LessonBuildError(
806
+ "Lesson cache directory is not writable. Tried: " + "; ".join(errors)
807
+ )
808
 
809
 
810
  def build_lesson(
 
860
  try:
861
  out_dir.mkdir(parents=True, exist_ok=True)
862
  except OSError as exc:
863
+ # Hard fallback: rebuild under /tmp so students still get a class.
864
+ emergency = (
865
+ Path(os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp")
866
+ / "docdoe-learn-lessons"
867
+ / key
868
+ )
869
+ try:
870
+ emergency.mkdir(parents=True, exist_ok=True)
871
+ out_dir = emergency
872
+ manifest_path = out_dir / "lesson.json"
873
+ logger.warning(
874
+ "Lesson cache mkdir failed (%s); using emergency path %s",
875
+ exc,
876
+ out_dir,
877
+ )
878
+ except OSError as exc2:
879
+ raise LessonBuildError(
880
+ f"Could not create lesson cache folder ({out_dir}): {exc}; "
881
+ f"emergency also failed: {exc2}"
882
+ ) from exc2
883
  delivery_mode = "reading" if mock_mode else "audio"
884
  delivery_notice = (
885
  "Local preview: this complete starter class is available in reading mode; no generated voice is being presented as real audio."