December 2025
“Professor AI”: Building an Autonomous Educational Intelligence Platform
Also published on Medium ↗Co-author- Samuel Estrada
At TETR College of Business, the volume of information is overwhelming. Between rapid-fire lectures, hundreds of PDFs, and hours of Zoom recordings, keeping up is a full-time job. I wanted to build a system that didn’t just “search” our course material but actually taught it like our professors. With their specific personalities, strict adherence to the curriculum, and deep context.
Most RAG (Retrieval-Augmented Generation) tutorials show you how to chat with a PDF. They don’t show you how to autonomously navigate a complex Learning Management System (LMS), scrape video lectures, transcribe them using multi-modal AI, and serve them via a hybrid search engine without paying for expensive enterprise APIs.
We built the Professor Agent Platform.
Here is the technical deep dive into how we constructed this system, using Selenium for resilience, Gemini for transcription, and a custom Hybrid RRF (Reciprocal Rank Fusion) engine for retrieval.
Pillar 1: The “Resilient” Harvester
The Challenge: Data Acquisition. Our college platform (“Coach”) is a complex multi-page application with dynamic loading, heavy JavaScript, and authenticated sessions. A simple BeautifulSoup script fails immediately. We needed an agent that acts like a human.
The “Resilient Find” Strategy
Standard Selenium scripts are brittle; if a CSS class changes, the bot breaks. We implemented a Multi-Strategy Locator pattern in src/harvester/navigation.py. When looking for a course section, the system doesn't rely on just one XPath. It attempts a waterfall of strategies:
Primary: Exact XPath matching the current DOM structure.
Fallback 1: Searching for specific aria-labels or data-testid attributes.
Fallback 2: “Text Proximity”, finding a header text (e.g., “Resources”) and traversing up to the parent container using generic relative XPath.
Fallback 3: JavaScript injection to query the Shadow DOM.

Session Persistence & Recovery
Logging in every time triggers security alerts and slows down testing. We implemented session state persistence. After a successful login, we serialized the browser’s cookies and localStorage to a JSON file (auth_state.json). On the next run, the driver injects these immediately.
If a navigation action fails due to a session timeout (e.g., a redirect to /login), the @resilient_session_action decorator catches the exception, performs a fresh login, saves the new state, and retries the original action transparently.
Pillar 2: The Multi-Modal Refinery
The Challenge: Video lectures are useless to an LLM without text. The platform handles this via a three-tier fallback system (src/refinery/recording_processor.py), designed to minimize costs while maximizing accuracy.
Tier 1: Native Extraction (Zero Cost)
Before trying to transcribe anything, the harvester inspects the DOM for existing captions.
- Zoom: It parses the transcript side-panel text directly.
- Google Drive: It intercepts the hidden network request to the timedtext API endpoint, decoding the JSON payload to get the official captions without playing the video.
Tier 2: The Gemini Fallback (The “Whisper Killer”)
If native captions are missing, the system enters “Generation Mode.” Instead of paying for OpenAI’s Whisper or Deepgram, we utilize the massive context window and multi-modal capabilities of Google’s Gemini 2.0 Flash.
- Heuristic Download: It resolves the video download URL using JavaScript injection.
- Audio Extraction: It uses ffmpeg (headless) to strip the video track, converting the file to a lightweight MP3.
- Transcription: It uploads the audio file directly to Gemini.
[Insert Image: Diagram illustrating the 3-Tier Transcription flow: Check DOM -> Extract Audio -> Gemini API.]
Tier 3: LLM Cleaning
Raw transcripts are messy (“Um, uh, so, like…”), (timestamps thorughout all recordings). We pipe the raw text into a LangChain chain to normalize the text for better embedding.
# src/refinery/cleaning.py
CLEANING_PROMPT = """You are an expert transcript editor. Your task is to clean up the following lecture transcript.
Remove filler words (um, uh, like, you know), fix grammatical errors, and ensure the text flows logically while preserving the original meaning and technical terms.
Do not summarize. Return the full cleaned text.
"""
Pillar 3: The “Secret Sauce” . Contextual Embeddings
A major flaw in standard RAG is Context Loss. If a chunk says “The margin is 20%,” the vector database doesn’t know which class or professor that belongs to.
In src/refinery/embedding.py, we intervene before the embedding step. We construct a Contextual Header that is prepended to every single text chunk:
# From src/refinery/embedding.py
def chunk_and_embed_text(clean_text: str, metadata: Dict[str, Any]):
global_context = _generate_context_summary(clean_text)
# Prepend context to the raw text before vectorizing
header_parts = [
f"Context Summary: {global_context}",
f"Course: {metadata.get('class_name')}",
f"Instructor: {metadata.get('teacher_name')}",
f"Date: {metadata.get('lecture_date')}"
]
context_header = "\n".join(header_parts) + "\n---\n"
# The vector now "knows" where it came from
doc.page_content = context_header + doc.page_content

When we embed this combined text, the vector representation encodes the metadata semantically. A query for “Professor Garima’s view on elasticity” will now mathematically align with this chunk, even if the text itself doesn’t mention her name.
Pillar 4: The Brain. Hybrid Search & Zero-Cost Re-ranking
This is where the system shines. We wanted enterprise-grade retrieval without paying for enterprise re-ranking APIs (like Cohere or Jina).
1. Hybrid Search with RRF (Reciprocal Rank Fusion)
Cosine similarity (Vector search) is great for concepts but bad for specific keywords (like “Section 409A”). Keyword search (BM25) is the opposite. We implemented Hybrid Search at the database level using Supabase (PostgreSQL).
To combine a “Cosine Distance” score with a “Keyword Frequency” score, We used Reciprocal Rank Fusion (RRF). This ignores the raw scores and looks at the rank (position) of the result.
The Formula:


Here is the SQL implementation We deployed:
-- database/match_documents_hybrid.sql
combined_scores AS (
SELECT
COALESCE(v.id, k.id) as id,
(COALESCE(1.0 / (rrf_k + v.rank_v), 0.0) +
COALESCE(1.0 / (rrf_k + k.rank_k), 0.0))::float as similarity
FROM vector_search v
FULL OUTER JOIN keyword_search k ON v.id = k.id
)
2. The Re-ranker: Custom MMR
Usually, after retrieval, developers send documents to an external Re-rank API (costing ~$1.00 per 1k searches). We built a function named cohere_rerank in src/shared/utils.py. It doesn't actually call Cohere. Instead, it implements a local algorithm called Maximal Marginal Relevance (MMR).
MMR maximizes relevance while penalizing redundancy. It selects the “Best” document, then looks for the next best document that is least similar to the one already picked.
# src/shared/utils.py
def maximal_marginal_relevance(query_embedding, doc_scores, lambda_param=0.7):
# ... logic ...
# MMR Score = (Relevance) - (Similarity to already selected)
score = lambda_param * relevance - (1 - lambda_param) * max_sim
By setting lambda_param=0.7, we tell the system: "Care 70% about being right, and 30% about telling me something new." This prevents the AI from getting 5 chunks that all say the exact same sentence.
Pillar 5: Intelligent Orchestration & Personas
Map-Reduce for Study Guides
A common failure mode in RAG is asking: “Give me a study guide for the whole course.” A standard retrieval will just fetch the top 20 random chunks, resulting in a fragmented answer.
In src/app/rag_core.py, we built an intent router. If the user asks for a summary, the system switches to a Map-Reduce workflow:
- Identify Topics: It scans the course to identify broad themes (e.g., “Market Analysis,” “Elasticity”).
- Map (Parallel Retrieval): It runs a targeted search for each identified topic individually.
- Reduce (Synthesize): It aggregates the summaries of all topics into one cohesive master guide.

Persona Injection
Finally, to make the system feel like a true “Professor,” we inject persona-specific instructions into the system prompt. For our “Market Gaps” course, the professor has a very specific, aggressive style.
Python
# src/app/rag_core.py
def _enforce_market_gaps_voice(answer: str) -> str:
# Replace generic "you" with "Boss"
content = re.sub(r"\byou\b", "Boss", content, flags=re.IGNORECASE)
# Ensure the response starts with affirmation
if not content.lower().startswith("okay"):
content = f"Okay Boss, {content}"
return f"{content}. Are you able to get it?"
This small touch transforms the AI from a generic bot into a familiar mentor for my cohort.
Pillar 6: Automation & Deployment
The system runs autonomously. We defined a GitHub Actions workflow in .github/workflows/daily_job.yml that runs every night at 02:30 UTC.
Running Selenium in a CI/CD container (Ubuntu) is difficult because there is no screen. We configured the runner to install xvfb (X Virtual Framebuffer) and libnss3. If the scraper crashes, it takes a screenshot of the error and uploads it as a GitHub Artifact so I can debug the UI changes the next morning.
The Stack:
- Backend: FastAPI on Render.
- Frontend: Lovable.dev (No-code React builder).
- Database: Supabase (pgvector).
- Orchestration: GitHub Actions.

Conclusion
This system has democratized access to our curriculum. Students no longer scrub through 2-hour Zoom recordings to find one definition. The Map-Reduce feature creates study guides that didn’t exist before.
By building this architecture from the ground up; using Hybrid Search, RRF, and local Re-ranking, we created an enterprise-grade search tool for the cost of a few API calls. It proves that with the right engineering, you don’t need a massive budget to build powerful AI tools.
“Professor AI”: Building an Autonomous Educational Intelligence Platform was originally published in Age of Awareness on Medium, where people are continuing the conversation by highlighting and responding to this story.