Live demo: getnextern.vercel.app
An explainable, content-based recommender that matches students to internships. Every company is described by the skills it has historically required, and a student is scored by how well they cover them. Nothing is a black box: every match shows the skills that matched, the ones that count partially through a related skill, and the ones still to learn.
The interface puts every company on one board. Each company module has the same three rows —
Have, Related (a skill in the same family counts half, e.g. Data science via Machine learning) and To learn — so you can compare all 11 at a glance. Light and dark themes
follow your system, with a toggle to switch.
- Add your skills. Type them (with autocomplete from the ontology), tap an example, or add an optional CGPA; your skills stay pinned beside the companies, the board re-sorts as you go, and skills the recommender doesn't know are flagged instead of being silently ignored.
- Read every company. All 11 companies, closest first, each with a fit band (strong / partial / reach), the skill breakdown and, for the top three, the next skill to learn. If none of your skills are recognised, it says so rather than ranking guesswork.
- Check a real posting. Paste any job description: its required skills are extracted and compared with your saved profile using the same breakdown.
- Share or reopen a map. Copy a link (
/?skills=python,sql&cgpa=8.2), or reopen any past map from History. Profile and history stay in your browser. - AI copilot (optional). With an API key on the server, an LLM reads pasted resume text into skills you review before anything is ranked, writes a learning plan for the gaps in your top matches, answers follow-up questions about your results, and rewords resume bullets for a chosen company without inventing experience. It only explains what the ontology computed; it never ranks companies itself.
- Skill ontology. Every skill is normalised through an alias map (
JS→ JavaScript,ML→ machine learning,React.js→ React) and grouped into families (skills.py), so related skills count partially — a React/Node profile gets half credit toward Vue. - Coverage scoring. For each company, every required skill is matched to the student's closest skill: an exact or alias match counts fully, a same-family skill counts half. The score is the average coverage, nudged slightly (15%) by how close the student's CGPA is to that company's past interns. Coverage maps to a fit band: 70%+ is a strong fit, 40%+ partial, anything lower a reach.
- Explanation and gaps. Each match returns three buckets — matched, related (with the family link) and gap (skills to learn) — the three rows on every company.
Why not embeddings? Static word embeddings were tried and rejected: on short tech jargon they scored
react↔vue≈ 0.05 andML↔machine learning≈ 0.29 — worse than the ontology. A transformer model would work but is too heavy for a free-tier deploy.
The dataset is small and synthetic — 51 students across 11 companies. A leave-one-out evaluation (each student removed from the company requirements before being scored) gives ~82% top-1 and ~100% top-3. Treat these as a sanity signal that skill coverage is a sensible ranking, not a production benchmark.
.
├── app.py # Flask: JSON API + serves the built frontend
├── recommender.py # Scoring engine (coverage, rank, explain, evaluate)
├── skills.py # Skill ontology: alias resolution + related-skill families
├── copilot.py # Optional LLM copilot (Anthropic or Gemini)
├── train.py # Builds the model bundle from the dataset
├── internship_data.csv
├── web/ # React + TypeScript frontend (Vite), builds into static/
│ # city photos (Wikimedia Commons) live in web/public/photos, credited on /about
├── tests/ # pytest suite for the recommender and the API
├── Dockerfile # Node stage builds web/, Python stage serves it
└── .github/workflows/ # CI: pytest, plus frontend tests and build
pip install -r requirements.txtBuild the frontend (Node 20+):
cd web && npm ci && npm run buildRun the app (the model bundle is built automatically on first run if missing):
python app.pyIt serves on http://localhost:7860. Set PORT to change the port and FLASK_DEBUG=1
for the reloader. For frontend work, run Flask and then npm run dev in web/; Vite
proxies /api to Flask (set API_PORT if Flask isn't on 7860).
Set ANTHROPIC_API_KEY to use Claude (claude-opus-5-5) or GEMINI_API_KEY for Google's
free tier (the model is discovered at runtime). Override the model with COPILOT_MODEL.
Without a key the copilot is switched off and says so; everything else works fully.
All endpoints return JSON with an ok flag.
POST /api/predict
{ "skills": "python, ml, klingon", "cgpa": 8.5 }{
"ok": true,
"recognised": ["python", "machine learning"],
"unrecognised": ["klingon"],
"cgpa": 8.5,
"recommendations": [
{
"company": "IBM",
"confidence": 86,
"coverage": 83,
"band": "strong",
"required_skills": ["data science", "machine learning", "python"],
"matched_skills": ["machine learning", "python"],
"related_skills": [{ "skill": "data science", "via": "machine learning" }],
"gap_skills": [],
"avg_rating": 3.2,
"location": "Bangalore",
"interns": 5
}
]
}| Endpoint | Body | Returns |
|---|---|---|
POST /api/predict |
{skills, cgpa} |
Recognised / unrecognised skills and every company ranked |
POST /api/match_jd |
{skills, jd_text} |
Coverage of a pasted posting with the skill breakdown |
GET /api/skills |
— | Every known skill, most common first |
POST /api/copilot/extract |
{text} |
A profile read from resume text, for review |
POST /api/copilot/plan |
{skills, cgpa} |
A learning plan for the top three matches' gaps |
POST /api/copilot/ask |
{skills, cgpa, question, history} |
An answer grounded in the student's matches |
POST /api/copilot/tailor |
{skills, resume_text, company | jd_text} |
Resume bullets reworded for one target |
GET /api/insights |
— | Dataset stats, company profiles, evaluation metrics |
GET /api/config |
— | Whether the copilot is on, and which provider |
GET /health |
— | Liveness check |
skills accepts a comma-separated string or a list; the older "Technical skills" /
"CGPA" keys still work. Copilot endpoints return 503 when no key is configured.
Requests are rate-limited per IP (predict 60/min, posting check 30/min, copilot 10–20/min).
pip install pytest && pytest -q
cd web && npm testdocker build -t nextern .
docker run -p 7860:7860 nexternDeployed on Vercel: vercel.json builds web/ into static/, and
app.py runs as a Python function that serves both the API and the built frontend. Pushing
to main redeploys automatically. The Dockerfile still runs the same app anywhere else.
MIT — see LICENSE.


