💡 Inspiration
Most people don't fail to learn because they lack motivation. They get stuck before the first lesson. "What career fits me?" turns into "what should I study?", then into 40 tabs of courses with no way to tell which are trustworthy, what order to take them in, or whether you already know half of it.
Career quizzes, course catalogs and generic roadmaps each solve one slice. We wanted one loop that does it all:
Describe yourself → discover careers → compare how far each one is → choose → verify what you already know → learn → build evidence → the route adapts
The project started as LearnFlow (you can still see LEARNFLOW_* env vars in the code) and grew into PathFinder. One rule stayed constant: a recommendation must be able to explain itself, and be honest about what it doesn't know.
🧭 What it does
- Career discovery from plain language. Write about your skills, interests and goals. PathFinder combines multi-provider AI with the live ESCO occupation taxonomy to surface careers, including ones that exist in no local catalog. Each result shows skill reuse, top gaps and a transition estimate in hours and weeks, not a mystery "92% match".
- Career comparison side by side on overlap, interest/work alignment, missing competencies and what transfers if you switch.
- A real roadmap. Picking a career saves a competency model: a prerequisite DAG, milestones, time ranges and a pan/zoom competency graph.
- Evidence over claims. Saying "I know Python" counts as weak evidence. Skills move through not started → building → developing → verified, and the planner inserts quick check-ins where it's unsure instead of making you repeat content.
- AI-taught micro-courses. Each resource opens 3–5 concept lessons with worked examples, an open-response checkpoint per concept, an application task and a teach-back. The next concept stays locked until the current answer passes a rubric-based AI grader.
- Glass-box recommendations. Every ranked resource ships its full score breakdown, credibility evidence and limitations.
🛠️ How we built it
Stack: Python FastAPI + Pydantic (~60 endpoints, about 5,400 lines of backend), MongoDB Atlas in production with SQLite for local dev behind the same storage layer, a dependency-light static frontend (vanilla JS + a TypeScript contracts file for type-checking), Docker, deployed on Render.
Bayesian mastery. Each skill keeps a Beta posterior. Evidence with score $s\in[0,1]$ and weight $w$ updates it:
$$ \alpha \leftarrow \alpha + s\,w,\qquad \beta \leftarrow \beta + (1-s)\,w,\qquad m=\frac{\alpha}{\alpha+\beta} $$
Confidence grows with the number of observations $n$ and the concentration of the posterior:
$$ c = 0.72\left(1-e^{-n/4.8}\right) + 0.28\,\min!\left(1,\frac{\alpha+\beta}{12}\right) $$
A skill is verified only when $m\ge$ its target and $c\ge 0.42$. Finishing a lesson contributes only weak evidence, so the separate skill check is what decides.
Explainable ranking. For each resource, the score is a transparent weighted sum, with weights loaded from a config file rather than buried in code:
$$ S = 0.22\,C + 0.12\,L + 0.09\,P + 0.09\,Q + 0.16\,M + 0.18\,K + 0.06\,T + 0.04\,N + 0.04\,F $$
(coverage, level fit, preference, quality, semantic similarity, credibility, time, session length, feedback). A greedy weighted set-cover step then picks courses by value per $\sqrt{\text{duration}}$ so a route doesn't recommend five courses that teach the same thing.
Prerequisites you can trust. Authored edges drive the route. Edges proposed by AI are display-only unless a second, independent verification pass clears them at a confidence threshold (0.72), or a human promotes them. A topological check enforces a DAG and automatically disables non-authored edges if a cycle ever appears.
Governed ML, from scratch. The course-retrieval model is a hashed TF-IDF with character trigrams trained on 8,462 CC BY / Public Domain OERCommons documents. It stores only aggregate weights, not text. A second, separately governed readiness model is a standardized logistic regression trained on 25,722 OULAD rows with sensitive demographic fields excluded in code. Both are loaded only if their SHA-256 matches a manifest, and the app falls back to deterministic retrieval otherwise.
Credibility as provenance. Each resource is scored from provider verification against a 21-entry source registry, official-domain match, license integrity, freshness and metadata. It's capped hard when evidence is missing, and the UI states plainly that this is not accreditation or a guarantee of outcomes.
Pace that adapts, carefully. Time estimates adjust to your observed focus sessions only after at least two completed sessions, clamped to a $0.72\text{–}1.28\times$ factor so noisy data can't distort the plan.
📚 What we learned
- Let AI propose; let rules decide. Models are great at generating careers, lessons and edges, but we keep a deterministic gate in front of anything that changes a learner's path.
- Don't oversell the numbers. Our retrieval model scores Hit@1 = 0.93 and MRR = 0.95, but on a title-to-own-document proxy, which we label clearly. The readiness model reaches ROC-AUC 0.74 on a group holdout by module-presentation, and it is not allowed to steer paths or make high-stakes decisions.
- Licensing is a feature. A source registry with rights, attribution and "link-only" modes decided what we could store, link, or never scrape.
- Explaining beats optimizing. Showing why an item was recommended mattered more to the UX than a point of accuracy.
🧗 Challenges we faced
- Prompt injection in a learning app. Free-text answers and a coach chat are an attack surface. We normalize Unicode and strip zero-width characters, run deterministic injection and off-topic checks before any model call, send only minimal lesson context, scan replies for leaked secrets, and instruct the grader to treat the learner's answer as untrusted data.
- Unreliable AI output. Generated lessons are strictly validated (at least 3 topics, rubrics with 2+ points, valid tasks) and rejected otherwise. A router rotates keys and models across Gemini, Groq, OpenRouter, Ollama and any OpenAI-compatible endpoint.
- Not letting a short path hijack relevance. Early versions ranked "easy" careers above the one the user actually asked for. We made relevance a gate: a transition-distance bonus can never rescue an unrelated career.
- Matching messy skill vocabulary. AI-written skill names rarely equal catalog names. A conservative alias and token-overlap matcher (minimum 0.46) widens coverage without pretending adjacent skills are exact. When nothing verified exists, we show official provider search links instead of inventing a course.
- Production hardening. Sessions use
HttpOnlycookies with scrypt-hashed passwords, CSRF tokens, cross-user (IDOR) blocking, per-IP and per-user rate limits, a strict CSP, and startup checks that refuse to boot in production without TLS Mongo, secure cookies or a verified model. - A deploy-day lesson. Our first Render deploy crashed at startup because the Atlas SRV hostname in the environment variable no longer resolved (
NXDOMAIN). It taught us to validate config early and surface clear errors.
🚀 What's next
- Import real learner-sequence evidence (with permission) so prerequisite edges can be confirmed by data.
- Fall back to the nearest reachable result when an exact career target can't be verified.
- Broaden the verified, openly licensed catalog across more fields.
Built With
- css3
- docker
- esco-api
- fastapi
- google-gemini
- groq
- html5
- javascript
- logistic-regression
- mongodb
- mongodb-atlas
- oercommons
- ollama
- openrouter
- oulad
- pydantic
- pymongo
- python
- render
- rest-api
- sqlite
- tf-idf
- typescript
- uvicorn