Files
matchmaking-v2/research/poc/auto-apply/app.py
raulgupta 89ca9ad647 Initial commit: matchmaking-v2 on-demand multi-board job-search agent
On-demand Scout service (replaces the nightly aggregator):
- FastAPI agent-mesh service; card name "matchmaking-service" (HTTP /a2a/tasks
  + Redis-stream worker matching the sibling-service pattern)
- run_search: parallel multi-board sweep (Naukri/blackfalcondata + Foundit +
  LinkedIn), city-filtered at the board, cheapest-per-result first
- per-board adapters + normalizers -> ScoutJob with rich read-only details and
  offsite apply links; recall mode + MVQ guard
- result cache for dev replay ($0); per-board cost knobs; retry-on-5xx
- research/: engine design, board cost economics, India-actor shortlist, POC
2026-06-18 19:22:03 +05:30

234 lines
8.6 KiB
Python

"""
Auto-apply POC — proves the "one tap at the end" works end-to-end.
What this proves (and what it does NOT):
✅ One tap -> backend assembles the staged application kit -> apply-router picks the
right mechanism (hosted ATS submit vs prepared board handoff) -> submits -> confirmation
-> tracked, with measured latency.
✅ The apply-router BRANCHING that is the architectural heart of the no-install web app.
❌ It does NOT submit to a real employer. The "ATS" here is a MOCK we control
(route: /mock-ats/...). POC-1 swaps ATS_SUBMIT_URL for a real ATS *sandbox* job.
HARD RULE: never fire test applications at real employers' live postings.
Run:
pip install fastapi uvicorn httpx
uvicorn app:app --port 8765
open http://localhost:8765
"""
from __future__ import annotations
import os
import time
import uuid
from typing import Literal
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
app = FastAPI(title="Scout auto-apply POC")
# Swap this for a real ATS sandbox endpoint in POC-1. Default = our local mock.
ATS_SUBMIT_URL = os.environ.get(
"ATS_SUBMIT_URL", "http://localhost:8765/mock-ats/greenhouse/acme-test/eng-42"
)
# ---------------------------------------------------------------------------
# Demo data: two jobs, one routed to an ATS (hosted submit), one to a board
# (prepared handoff). In production these come from the on-demand listings fetch.
# ---------------------------------------------------------------------------
JOBS = {
"job_ats": {
"title": "Product Analyst",
"company": "Acme (Greenhouse)",
"source": "ats", # -> hosted submit
"ats_vendor": "greenhouse",
},
"job_board": {
"title": "Associate PM",
"company": "Globex (Naukri)",
"source": "board", # -> prepared handoff
"board": "naukri",
"apply_url": "https://www.naukri.com/job-listings-associate-pm-globex",
},
}
# The "application kit" — staged BEFORE the tap. In production: tailored résumé from
# resume-builder + field answers from the user's profile.
KIT = {
"candidate": {
"first_name": "Asha",
"last_name": "Rao",
"email": "asha.rao@example.com",
"phone": "+91-90000-00000",
},
"resume_url": "https://files.growqr.ai/resumes/asha-rao-tailored.pdf",
"answers": {
"years_experience": "3",
"work_authorization": "India - citizen",
"notice_period_days": "30",
},
}
# In-memory application tracker (the "pipeline" view).
TRACKER: list[dict] = []
class ApplyResult(BaseModel):
status: Literal["submitted", "prepared"]
mechanism: Literal["hosted_ats", "board_handoff"]
job_id: str
confirmation_id: str | None = None
deeplink: str | None = None
staged: dict | None = None
latency_ms: int
@app.get("/api/kit/{job_id}")
def get_kit(job_id: str):
"""The frontend shows 'kit ready' before the tap."""
if job_id not in JOBS:
raise HTTPException(404, "unknown job")
job = JOBS[job_id]
return {
"job": {"id": job_id, "title": job["title"], "company": job["company"]},
"ready": {
"resume": True,
"cover_letter": True,
"fields_filled": len(KIT["answers"]),
},
"mechanism": "hosted_ats" if job["source"] == "ats" else "board_handoff",
}
@app.post("/api/apply/{job_id}", response_model=ApplyResult)
async def apply(job_id: str):
"""THE ONE TAP. Routes to hosted submit or prepared handoff."""
if job_id not in JOBS:
raise HTTPException(404, "unknown job")
job = JOBS[job_id]
t0 = time.perf_counter()
if job["source"] == "ats":
# Hosted submit: backend POSTs the full application to the ATS API.
payload = {
"first_name": KIT["candidate"]["first_name"],
"last_name": KIT["candidate"]["last_name"],
"email": KIT["candidate"]["email"],
"phone": KIT["candidate"]["phone"],
"resume_url": KIT["resume_url"],
"answers": KIT["answers"],
}
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(ATS_SUBMIT_URL, json=payload)
if resp.status_code >= 400:
raise HTTPException(502, f"ATS rejected: {resp.status_code} {resp.text}")
confirmation_id = resp.json().get("confirmation_id")
latency_ms = int((time.perf_counter() - t0) * 1000)
result = ApplyResult(
status="submitted",
mechanism="hosted_ats",
job_id=job_id,
confirmation_id=confirmation_id,
latency_ms=latency_ms,
)
else:
# Prepared handoff: stage everything, hand the user a deep link to the board.
latency_ms = int((time.perf_counter() - t0) * 1000)
result = ApplyResult(
status="prepared",
mechanism="board_handoff",
job_id=job_id,
deeplink=job["apply_url"],
staged={"resume_url": KIT["resume_url"], "answers": KIT["answers"]},
latency_ms=latency_ms,
)
TRACKER.append(
{
"job_id": job_id,
"company": job["company"],
"status": result.status,
"confirmation_id": result.confirmation_id,
}
)
return result
@app.get("/api/tracker")
def tracker():
return {"applications": TRACKER}
# ---------------------------------------------------------------------------
# MOCK ATS — stands in for Greenhouse/Lever/Ashby. POC-1 deletes this and points
# ATS_SUBMIT_URL at a real *sandbox* job. The contract mirrors Greenhouse's job-board
# application submission shape (first/last/email/phone/resume + question answers).
# ---------------------------------------------------------------------------
@app.post("/mock-ats/greenhouse/{board}/{job}")
def mock_ats_submit(board: str, job: str, payload: dict):
required = ["first_name", "last_name", "email", "resume_url"]
missing = [f for f in required if not payload.get(f)]
if missing:
raise HTTPException(422, f"missing required fields: {missing}")
return {
"accepted": True,
"board": board,
"job": job,
"confirmation_id": f"GH-{uuid.uuid4().hex[:10]}",
}
@app.get("/", response_class=HTMLResponse)
def index():
return INDEX_HTML
INDEX_HTML = """<!doctype html>
<html><head><meta charset="utf-8"><title>Scout — one-tap apply POC</title>
<style>
body{font-family:system-ui,sans-serif;max-width:560px;margin:40px auto;color:#1a1a1a}
.card{border:1px solid #eee;border-radius:14px;padding:18px;margin:14px 0;box-shadow:0 1px 4px #0001}
.ready{color:#0a7d34;font-size:14px}
button{background:#ea580c;color:#fff;border:0;border-radius:10px;padding:12px 18px;font-size:16px;cursor:pointer}
button:disabled{opacity:.6}
.out{font-family:ui-monospace,monospace;font-size:13px;background:#faf7f5;border-radius:8px;padding:10px;margin-top:10px;white-space:pre-wrap}
h2{margin:.2em 0}
</style></head><body>
<h1>🕵️ Scout — one-tap apply</h1>
<p>Everything is staged before the tap. The tap <b>ends</b> the application.</p>
<div id="cards"></div>
<script>
const JOBS = ["job_ats","job_board"];
async function load(){
const root = document.getElementById('cards'); root.innerHTML='';
for(const id of JOBS){
const kit = await (await fetch('/api/kit/'+id)).json();
const div = document.createElement('div'); div.className='card';
div.innerHTML = `<h2>${kit.job.title}</h2><div>${kit.job.company}</div>
<div class="ready">✓ Résumé ready · ✓ Cover letter ready · ✓ ${kit.ready.fields_filled} fields filled
· ${kit.mechanism==='hosted_ats'?'🛰️ hosted submit':'🔗 prepared handoff'}</div>
<p><button onclick="apply('${id}',this)">🚀 Apply (one tap)</button></p>
<div class="out" id="out-${id}" style="display:none"></div>`;
root.appendChild(div);
}
}
async function apply(id,btn){
btn.disabled=true; btn.textContent='';
const r = await (await fetch('/api/apply/'+id,{method:'POST'})).json();
const out = document.getElementById('out-'+id); out.style.display='block';
if(r.status==='submitted'){
out.textContent = `✅ APPLIED — confirmation ${r.confirmation_id}\\nmechanism: ${r.mechanism} · ${r.latency_ms}ms`;
btn.textContent='✓ Applied';
} else {
out.textContent = `🔗 PREPARED — tap to finish on the board:\\n${r.deeplink}\\nstaged résumé + ${Object.keys(r.staged.answers).length} answers · ${r.latency_ms}ms`;
btn.textContent='Open board →'; btn.disabled=false;
btn.onclick=()=>window.open(r.deeplink,'_blank');
}
}
load();
</script>
</body></html>"""