Inspiration

Every internship application wants a tailored resume, and tailoring by hand costs about an hour per company. The obvious shortcut — pasting your resume into a chatbot — has a darker failure mode: early in this project, our own agent confidently added LangChain to a bullet describing an experience that never used it. That moment reframed the whole build. A resume tool that invents facts isn't a productivity tool; it's a fraud generator. So the goal became: an agent that tailors fast, but is architecturally constrained from lying about you.

What it does

A conversational agent (Google ADK + Gemini) that walks a five-phase workflow: parse your resume → score it against a job description → interview you for hidden, quantifiable achievements → apply only the edits you approve → render a polished PDF and save it as a named version in MongoDB Atlas — one per company and role. Saved versions can be compared, reloaded as the working draft, and downloaded straight from the history panel. The agent suggests; the user decides. Every time.

How we built it

Two layers of Gemini: an orchestrator (gemini-2.5-pro) that decides which tool to call, and ten specialized tools — parser, JD matcher, a generator–validator bullet rewriter, a path-based structural editor, formatting checker, Jinja2/Playwright renderer, and MongoDB versioning — behind a Streamlit frontend with live tool-progress streaming. Three rules became the architecture:

  1. The resume never travels through the LLM. It lives in ADK session state; tools read and write it in code. The model only passes small arguments like bullet_id or jd_text.
  2. LLM decides, code executes. Rubric validation, suggestion sorting, schema normalization, and path whitelisting are deterministic code — the model is trusted with judgment, never with bookkeeping.
  3. Truthfulness is enforced, not requested. The rewriter validates every bullet against a rubric and is hard-banned from inventing metrics; when data is missing, the agent asks instead.

Challenges we ran into

  • MALFORMED_FUNCTION_CALL crashes: passing the full resume JSON as a function-call argument made the model corrupt it, and re-sending it every turn snowballed the context (every token re-processed at prefill). Moving the resume into session state killed the crashes, the schema drift, and the bloat in one stroke.
  • The agent writing checks its tools couldn't cash: our instruction promised "reorder entries" and "add a bullet" with no tools behind them. The model didn't fail gracefully — it improvised, once by rebuilding the entire resume text and re-parsing it (silently destroying earlier edits), once by misattributing one project's experience under another project's name. The fix was a line-by-line audit of every promise in the prompt against the real tool surface, plus a structural CRUD editor so the legitimate path exists.
  • Classic integration landmines: sync Playwright exploding inside an async event loop (solved with subprocess isolation), a PDF upload that hung because base64 was being "recited" through the model token by token (solved by extracting text locally), and two dependency-version clashes producing misleading tracebacks deep inside Google libraries.

Accomplishments that we're proud of

  • A truthfulness design that survived real adversarial moments: when asked to quantify a bullet with no data, the agent asks for the real number instead of inventing one — verified end-to-end, not just claimed.
  • Closing the full versioning loop on MongoDB Atlas: save → list → compare → reload as the working draft → per-version PDF download, with the resume kept out of the LLM's context the entire way.
  • Eliminating an entire crash class (MALFORMED_FUNCTION_CALL) by an architectural move rather than retries or model upgrades.
  • A bilingual engineering log of every pitfall and decision — the repo documents not just what we built, but why.

What we learned

  • Docstrings are prompts. ADK feeds tool docstrings to Gemini as its manual — a lying docstring poisons behavior exactly like a lying system prompt.
  • Gray zones are invitations. Anything the prompt neither promises nor forbids, the model will eventually improvise — sometimes dishonestly. Every capability must be either backed by a real tool or explicitly declared impossible.
  • Verify on the real path. Offline tests with fake contexts passed while the real orchestrator was crashing; LLM-orchestration bugs only reproduce through the actual runner.
  • Prompt changes are probabilistic steering, not control flow — the honest claim is "constrained the behavior", never "fixed it 100%".

What's next for Resume Builder Agent

A deployment checklist for Cloud Run: per-user version isolation (the user_session field already exists in the schema — only the wiring remains), a self-serve clear-history button, secrets via Secret Manager, and Playwright baked into the container image. Then the speed work: collapsing the two-layer LLM latency that streaming progress currently makes feel fast — disabling tool-level thinking and routing the first parse around the orchestrator.

Built With

  • gemini
  • google-adk
  • jinja
  • mongodb-atlas
  • pdfplumber
  • playwright
  • pymongo
  • python
  • streamlit
  • vertex-ai
Share this project:

Updates