employee_feedback_AI: The AI HR Tool That Refuses to Depend on One Model
A FastAPI and React app that combines offline sentiment rules, structured LLM output, and lightweight RAG to turn raw employee feedback into something HR can actually use.
- This repo treats AI as a reliability problem first, then a model problem second.
- Its real strength is a fallback ladder that starts with offline heuristics and upgrades to structured LLM output only when needed.
- The RAG layer stays lightweight on purpose, using local text files and TF-IDF instead of a heavier vector database stack.
- The whole system is shaped to become a record, not just a response, which makes the output usable for HR workflows and dashboards.
The app is built like a fallback ladder, not a single AI call
Most AI products begin with the model and hope the product follows. This repo starts the other way around. It makes sure feedback can still be classified, routed, and stored even when the LLM path is unavailable, which is exactly what an internal HR tool needs if it wants to feel dependable.
That design choice is the headline. The offline path gives the app a floor, the Gemini path raises the ceiling, and the database layer makes both paths useful in practice. The result is not flashy, but it is unusually disciplined for an AI-flavored prototype.
Offline analysis gives the app a floor
The offline analyzer in ai_agent.py is not trying to be clever. It combines VADER sentiment scoring with keyword buckets for topics and urgency, then turns that into a simple classification pass. That is enough to keep the app functional with no internet, no API key, and no vector infrastructure.
That is the point. In a lot of AI tools, the baseline mode is basically a blank screen with a disabled button. Here, the fallback is a real product behavior. It can still sort feedback into something useful, which is a far more honest definition of resilience.
# Simplified shape of the offline path
sentiment = score_with_vader(text)
topics = detect_topic_keywords(text)
urgency = classify_urgency(sentiment, topics)
recommendation = generate_rule_based_recommendation(sentiment, urgency, topics)
return {
"sentiment": sentiment,
"topics": topics,
"urgency": urgency,
"recommendation": recommendation,
}
Gemini is used as a structured upgrade, not a freeform oracle
The online path in gemini_ai.py is more interesting than a plain prompt wrapper because it is forced to speak in schema-shaped JSON. The response is constrained by a structured output contract, which means the model has to produce fields that downstream code can trust and persist.
That matters because the app is not presenting AI as a chat toy. It is trying to store sentiment, urgency, and recommendations in database columns. Once the model has to behave like a typed service, the whole system becomes easier to reason about.
The RAG layer is deliberately lightweight
The retrieval layer in rag.py avoids the usual heavy stack. Instead of a vector database, it uses local text files, TF-IDF, and cosine similarity to pull relevant policy passages from the knowledge base. That makes the feature easy to ship, easy to inspect, and easy to run in a small deployment.
This is where the article stops being about AI hype and becomes about practical trade-offs. A folder of plain text files is enough to answer a narrow class of HR questions if the retrieval logic is simple and honest about what it can do. It is a smarter move than overbuilding the infrastructure around a modest use case.
| Dimension | This repo | Heavier alternative | Why it matters |
|---|---|---|---|
| Setup cost | Local text files, TF-IDF, SQLite-friendly | Vector database, embedding pipeline, more services | Less infrastructure means faster adoption. |
| Offline resilience | Baseline analysis works without the cloud | Often depends on live model calls | Internal tools fail less often when the floor still functions. |
| Output reliability | Structured JSON and schema-shaped fields | Freeform prompts with fragile parsing | Typed outputs are easier to store and query. |
| Retrieval complexity | Chunk text and rank by cosine similarity | Embedding index plus external store | Simplicity keeps the feature understandable and cheap. |
| Production risk | Narrower surface area and fewer moving parts | More vendor and latency dependency | Fewer dependencies usually means fewer surprises. |
Feedback becomes a record, not just a prompt
The backend orchestration in main.py is where the product starts behaving like a system of record. Feedback is submitted, analyzed, and then stored with fields such as sentiment, urgency, and recommendation. That turns a one-off text response into something HR can query, filter, and track over time.
This is the invisible difference between a demo and an internal tool. A prompt can answer a question once. A database record can power dashboards, trend lines, and follow-up workflows. The repo is clearly aiming for the second outcome.
# Simplified orchestration shape
analysis = analyze_feedback(feedback_text)
record = Feedback(
text=feedback_text,
sentiment=analysis["sentiment"],
urgency=analysis["urgency"],
recommendation=analysis["recommendation"],
)
db.add(record)
db.commit()
| System goal | Prompt-only AI | This repo |
|---|---|---|
| User interaction | Returns a response | Returns a response and a stored record |
| Operational value | Hard to audit later | Queryable by HR admins |
| Failure mode | One bad call breaks the moment | Fallback logic keeps the app usable |
| Analytics | Usually external or manual | Native to the stored schema |
The frontend is doing quiet but important work
The React side does not try to impress. It manages JWT attachment in the API client, switches base URLs based on environment, and keeps the app connected to the backend without making the user think about it. That is exactly what production-ish internal software should do.
It is easy to dismiss this layer as plumbing, but that misses the point. Internal tools win when the boring parts are stable. Authentication, request routing, and environment awareness are what make the AI features feel deployable instead of fragile.
// Representative client behavior
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
const baseURL = import.meta.env.DEV
? "http://localhost:8000"
: import.meta.env.VITE_API_URL;
What this repo gets right, and what still marks it as MVP
The strongest thing about this project is its discipline. It uses lightweight primitives where they are enough, and only reaches for more AI when the architecture can absorb it. That keeps the repo small, understandable, and surprisingly resilient for a feature-rich prototype.
It is still obviously early. SQLite is the default, the security surface deserves hardening, and the presence of a hardcoded secret is the kind of thing a production review would flag immediately. But those rough edges do not weaken the thesis. They make it more believable: this is a serious MVP, not a finished platform.
| Maturity signal | What it suggests | Why it matters |
|---|---|---|
| SQLite default | Simple local-first setup | Good for development, not the final scaling story |
| Test files present | Core paths are being checked | Reliability is part of the design, not an afterthought |
| Hardcoded secret | Security still needs work | The app is not production-hardened yet |
| Phased code comments | Built in deliberate stages | The repo is educational as well as functional |