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.

8 min read View on GitHub More from Hariharan22-hub

A split mechanical workstation on a white background. On one side, feedback cards pass through a simple rule-based sorting machine with gauges and labeled bins. On the other, the same stream is stamped into orderly filing drawers by a more precise inspection rig. It explains that the app uses a fallback AI path and an upgraded structured path.
The repo is built like a ladder of capability. It always has a baseline answer, then adds structure and retrieval when the higher tiers are available.
Key Takeaways

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.

The architecture is less a single pipeline than a set of capability levels. Each layer adds value without making the one beneath it irrelevant.

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.

A close-up of a metal inspection press stamping tidy labels onto aligned filing cards. One card is entering as loose text, while the other emerges as a structured record with clean fields. It explains how the app forces Gemini output into database-shaped JSON.
The LLM tier is not allowed to ramble. It has to emit structured output that the backend can persist without guesswork.

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.

DimensionThis repoHeavier alternativeWhy it matters
Setup costLocal text files, TF-IDF, SQLite-friendlyVector database, embedding pipeline, more servicesLess infrastructure means faster adoption.
Offline resilienceBaseline analysis works without the cloudOften depends on live model callsInternal tools fail less often when the floor still functions.
Output reliabilityStructured JSON and schema-shaped fieldsFreeform prompts with fragile parsingTyped outputs are easier to store and query.
Retrieval complexityChunk text and rank by cosine similarityEmbedding index plus external storeSimplicity keeps the feature understandable and cheap.
Production riskNarrower surface area and fewer moving partsMore vendor and latency dependencyFewer 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 goalPrompt-only AIThis repo
User interactionReturns a responseReturns a response and a stored record
Operational valueHard to audit laterQueryable by HR admins
Failure modeOne bad call breaks the momentFallback logic keeps the app usable
AnalyticsUsually external or manualNative 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 signalWhat it suggestsWhy it matters
SQLite defaultSimple local-first setupGood for development, not the final scaling story
Test files presentCore paths are being checkedReliability is part of the design, not an afterthought
Hardcoded secretSecurity still needs workThe app is not production-hardened yet
Phased code commentsBuilt in deliberate stagesThe repo is educational as well as functional