Inspiration
International graduates in Germany face a brutal job hunt that German citizens never see: every posting is a hidden gauntlet of visa sponsorship, Blue Card salary floors, German-language expectations, and relocation constraints. Generic job boards rank by keyword match and quietly surface roles that will reject you at the visa stage. We wanted an agent that thinks the way a good immigration-aware career coach does — one that reasons about whether a job is actually attainable for this candidate, explains why, and gets better the more it's used.
What it does
CareerPilot is an autonomous job-matching agent. You upload a CV (or paste text), it extracts a structured candidate profile, then it doesn't just rank jobs in one shot — it runs an agent loop:
- Plans a ranking strategy specific to the candidate (e.g. "junior Python eng, Berlin, needs visa → weight sponsorship and seniority fit heavily").
- Ranks jobs against a rubric, with autonomous retry if its own output fails schema validation.
- Self-evaluates each ranking with an LLM judge scoring fit, risk detection, and actionability.
- Self-improves: if the evaluation falls below threshold, it rewrites its own ranking rubric and re-ranks — looping until it's good enough or it hits its iteration budget, then stops on its own.
- Learns across runs: it persists the winning rubric and run history to Arize Phoenix, so the next run starts smarter than the last.
Every step streams live into an "Agent Console" so you watch it plan, call tools, score itself, and improve — not a black box.
How we built it
The agent spine lives in orchestrator.py and runs on Gemini 2.5 Flash via
Google Vertex AI (Google Gen AI SDK, vertexai=True) — planning, ranking,
evaluation, and rubric self-improvement are all Gemini calls. Structured outputs
are Pydantic-validated with a self-correcting retry wrapper that re-prompts the
model with its own error on a parse failure.
Our partner superpower is the Arize Phoenix MCP server. At runtime the app
spawns @arizeai/phoenix-mcp over stdio using the Python MCP SDK and calls its
tools directly: get-latest-prompt and get-spans to recall the last learned
rubric and recent run history, and upsert-prompt to persist the improved rubric
as a versioned Phoenix prompt. That prompt store is the agent's long-term
memory. We also added a Gemini function-calling turn so the model itself decides
whether to query Phoenix memory or inspect the job pool before planning. On top of
that, Phoenix OpenTelemetry tracing emits a span for every agent step, so the
whole reasoning trace is observable in the Phoenix dashboard.
The frontend is Streamlit with custom CSS and a live-streaming agent console. We deployed to Google Cloud Run with a single container that bundles both Python and Node, so the Phoenix MCP server runs as a real subprocess on the hosted URL — secrets via Secret Manager, build via Cloud Build.
Challenges we ran into
- Making it genuinely agentic, not a pipeline. The MVP was a straight line: extract → rank → done. We had to add real planning, model-selected tools, an evaluator-triggered improvement loop, and autonomous stopping criteria.
- Phoenix MCP for real, at runtime. Wiring an async MCP stdio session into a synchronous Streamlit app meant bridging event loops on a background thread. Phoenix also silently strips hyphens from prompt names, which cost us an hour until we switched our memory key to underscores.
- Cross-run memory without a "write annotation" tool. Phoenix MCP has no write-span-annotation tool, so we built persistent memory on versioned prompts instead — encoding the learned rubric as the prompt the next run reads back.
- Cloud Run with a Node subprocess. Getting
npx-spawned MCP to run inside a Python container, plus untangling a chain of IAM permissions for Cloud Build and Vertex AI, was the biggest time sink under deadline pressure.
Accomplishments that we're proud of
- A real self-improvement loop we can demonstrate: a deliberately weak first ranking scores 3/5, the agent rewrites its rubric, re-ranks, and reaches 5/5 — on camera, autonomously.
- Memory that actually persists across runs through the Phoenix prompt store,
not a mocked stub. The old
get_previous_feedback_for_sessionreturned[]; now it returns real Phoenix history. - A fully observable agent — every plan, tool call, and self-evaluation is a Phoenix span — deployed live on Cloud Run with the MCP server running in-container.
What we learned
The line between "LLM pipeline" and "agent" is planning, tool choice, and the ability to judge and correct its own work — and that's mostly engineering, not prompting. We learned how to integrate an MCP server as a first-class runtime dependency rather than a config-file afterthought, and that good observability (Phoenix tracing) is what makes a self-improving loop trustworthy instead of mysterious — you can see exactly why the agent changed its mind.
What's next for Career Pilot
- Richer memory: cluster learnings by candidate archetype so the agent recalls the most relevant past runs, not just the latest rubric.
- Live sourcing: pull fresh postings via search APIs and let the agent decide what to fetch.
- Feedback as a first-class signal: feed user thumbs-up/down back through Phoenix so the rubric improves from real outcomes, not just self-evaluation.
- Beyond Germany: generalize the visa/work-authorization reasoning to other countries' immigration rules.
Built With
- arize-phoenix
- cloud-build
- cloud-run
- docker
- gemini-2.5-flash
- google-cloud
- google-gemini
- mcp
- model-context-protocol
- node.js
- opentelemetry
- pydantic
- python
- secret-manager
- streamlit
- vertex-ai
Log in or sign up for Devpost to join the conversation.