Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
feat(rag): Layer 0 RAG pipeline - BU catalog scrape, embedding, and per-message retrieval#282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
01ac4ee1f3f7c2496582bFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| { | ||
| "total_courses": 8720, | ||
| "total_errors": 50, | ||
| "errors": [ | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-650a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-512a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pe-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-en-522a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-or-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-529a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-640a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-512a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-os-520a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-641a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-525a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pe-521a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pd-640a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pd-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pe-640a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-od-642a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-520a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-os-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-511a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-od-522a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-522a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-541a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-os-532a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-660a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-521a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-od-531a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-523a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-en-521a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-od-644a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-581a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pa-530a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-534a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-md-531a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-os-521a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-546a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-542a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-en-640a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-642a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-519a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-pe-520a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-527a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-521a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-642a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-os-640a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-544a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-gd-540a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-524a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-ph-524a/", | ||
| "error": "empty response" | ||
| }, | ||
| { | ||
| "url": "https://www.bu.edu/academics/sdm/courses/sdm-rs-532a/", | ||
| "error": "empty response" | ||
| } | ||
| ], | ||
| "elapsed_seconds": 1890, | ||
| "completed_at": "2026-06-27T01:39:55.726124+00:00", | ||
| "semester_tag": "fall_2026", | ||
| "output_file": "C:\\Users\\Jack\\Desktop\\VS Code\\sapling\\backend\\data\\bu_catalog_fall_2026.json" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win Remove the machine-local absolute path from this committed artifact.
🤖 Prompt for AI Agents | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -785,6 +785,7 @@ async def event_stream(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("invalidate_study_guide_cache", _invalidate_study_guide_cache, user_id, course_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("update_course_context", update_course_context, course_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("check_upload_achievements", _check_upload_achievements, user_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("index_document_chunks", _index_document_chunks, doc_id, course_id, user_id, extracted_text, classification.category, getattr(summary, "abstract", "")), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield sapling_event_to_sse(SaplingEvent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -898,6 +899,124 @@ def _check_upload_achievements(user_id: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _chunk_text(text: str, chunk_size: int = 800, overlap: int = 100) -> list[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Split text into overlapping character-window chunks.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while start < len(text): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end = start + chunk_size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks.append(text[start:end].strip()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start += chunk_size - overlap | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [c for c in chunks if len(c) > 50] # drop near-empty tail chunks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _index_document_chunks( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| course_id: str, # Sapling UUID — resolved to BU code internally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extracted_text: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| category: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc_summary: str = "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Chunk, embed, and upsert a document into course_chunks. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Runs in a background thread via _spawn_post_roll after the document | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is persisted, so it never blocks the SSE stream. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google import genai as _genai | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.genai import types as genai_types | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from db.connection import table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os, time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_COURSE_RELEVANCE = 0.35 # below this, document is likely off-topic for the course | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Resolve BU course code from Sapling UUID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows = table("courses").select( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "course_code", filters={"id": f"eq.{course_id}"}, limit=1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bu_course_id = (rows[0].get("course_code") or course_id) if rows else course_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks = _chunk_text(extracted_text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not chunks: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _gclient = _genai.Client(api_key=os.getenv("GEMINI_API_KEY", "")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _embed_texts(texts: list[str]) -> list[list[float]]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp = _gclient.models.embed_content( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model="gemini-embedding-001", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents=texts, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config=genai_types.EmbedContentConfig(output_dimensionality=768), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [list(e.values) for e in resp.embeddings] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ── Relevance gate ──────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fetch the catalog chunk embedding for this course and compare against | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the document's first chunk. Irrelevant documents are skipped to keep | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the index clean. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catalog_rows = table("course_chunks").select( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "embedding", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filters={"course_id": f"eq.{bu_course_id}", "category": "eq.catalog"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| limit=1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if catalog_rows and catalog_rows[0].get("embedding"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catalog_vec = catalog_rows[0]["embedding"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Use the AI-generated summary as the document representative — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # it's more reliable than raw first-chunk text (avoids cover pages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # tables of contents, and boilerplate skewing the score). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sample_text = doc_summary or chunks[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc_sample_vec = _embed_texts([sample_text])[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| time.sleep(1.5) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # cosine similarity (vectors are unit-norm from the model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dot = sum(a * b for a, b in zip(doc_sample_vec, catalog_vec)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if dot < MIN_COURSE_RELEVANCE: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+960
to
+975
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Compare against the best catalog match, not an arbitrary catalog row. Line 960 fetches Suggested adjustment- catalog_rows = table("course_chunks").select(- "embedding",- filters={"course_id": f"eq.{bu_course_id}", "category": "eq.catalog"},- limit=1,- )- if catalog_rows and catalog_rows[0].get("embedding"):- catalog_vec = catalog_rows[0]["embedding"]+ catalog_rows = table("course_chunks").select(+ "embedding",+ filters={"course_id": f"eq.{bu_course_id}", "category": "eq.catalog"},+ )+ catalog_vecs = [r["embedding"] for r in catalog_rows if r.get("embedding")]+ if catalog_vecs:
# Use the AI-generated summary as the document representative —
# it's more reliable than raw first-chunk text (avoids cover pages,
# tables of contents, and boilerplate skewing the score).
sample_text = doc_summary or chunks[0]
doc_sample_vec = _embed_texts([sample_text])[0]
time.sleep(1.5)
- # cosine similarity (vectors are unit-norm from the model)- dot = sum(a * b for a, b in zip(doc_sample_vec, catalog_vec))+ dot = max(sum(a * b for a, b in zip(doc_sample_vec, catalog_vec)) for catalog_vec in catalog_vecs)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "[RAG] doc %s skipped — relevance to %s is %.3f (< %.2f)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc_id, bu_course_id, dot, MIN_COURSE_RELEVANCE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| records = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, chunk_text in enumerate(chunks): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw = f"{doc_id}::{i}::{chunk_text}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cid = hashlib.sha256(raw.encode()).hexdigest() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| records.append({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": cid, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "course_id": bu_course_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "doc_id": doc_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "uploader_id": user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "chunk_index": i, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "chunk_text": chunk_text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "chunk_hash": cid, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "embedding": None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "category": category, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "semester": "current", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "section_id": None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "school": "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Embed in batches of 50 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BATCH = 50 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in range(0, len(records), BATCH): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| batch = records[i : i + BATCH] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| texts = [r["chunk_text"] for r in batch] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vecs = _embed_texts(texts) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for rec, vec in zip(batch, vecs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec["embedding"] = vec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning("[RAG] embed failed for doc %s batch %d: %s", doc_id, i, e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| time.sleep(1.5) # stay under 3000 req/min quota | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table("course_chunks").upsert(records, on_conflict="id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+994
to
+1014
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Only upsert successfully embedded chunks, and keep upserts batched. If Suggested adjustment for i in range(0, len(records), BATCH):
batch = records[i : i + BATCH]
texts = [r["chunk_text"] for r in batch]
try:
vecs = _embed_texts(texts)
+ if len(vecs) != len(batch):+ raise ValueError(f"embedding count mismatch: {len(vecs)} for {len(batch)} chunks")
for rec, vec in zip(batch, vecs):
rec["embedding"] = vec
+ table("course_chunks").upsert(batch, on_conflict="id")
except Exception as e:
logger.warning("[RAG] embed failed for doc %s batch %d: %s", doc_id, i, e)
time.sleep(1.5) # stay under 3000 req/min quota
- table("course_chunks").upsert(records, on_conflict="id")
logger.info("[RAG] indexed %d chunks for doc %s", len(records), doc_id)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("[RAG] indexed %d chunks for doc %s", len(records), doc_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.exception("[RAG] _index_document_chunks failed for doc %s", doc_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _spawn_post_roll(*tasks: tuple) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Fire-and-forget post-roll work for SSE / non-FastAPI-BackgroundTasks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contexts. Each tuple is (label, callable, *args). Exceptions in the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Don't commit a scrape summary that already records 50 failed course pages.
Ingestion uses this scrape output as the source corpus, so these unrecovered SDM URLs mean the initial RAG index is knowingly incomplete. Please either rerun until this is clean or make ingestion fail fast when the scrape summary reports unresolved errors.
🤖 Prompt for AI Agents