Files
matchmaking-v2/research/poc/browser-apply/apply_ashby.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

205 lines
12 KiB
Python

"""
apply_ashby.py — master the Ashby application in ISOLATION (no LinkedIn).
Goes DIRECTLY to the OpenAI / AshbyHQ application URL. That page is public — no login, so no
LinkedIn new-device alerts and no ban vector. "5 applications ≠ 5 logins": here it's 0 logins.
This is the focused "master Ashby" script. The LinkedIn discovery+login flow is preserved as a
COMMENTED block below (we'll re-enable it and merge the two into one unit once Ashby is solid).
ACCURACY IS GO/NO-GO: the agent must satisfy a hard checklist — NO required field left empty —
before it will submit or hand off. It reports every required field and whether all were filled.
Run:
ASHBY_URL="https://jobs.ashbyhq.com/openai/<id>/application" python apply_ashby.py
AUTO_SUBMIT=0 ASHBY_URL="..." python apply_ashby.py # fill + gate, STOP before submit (you submit)
"""
from __future__ import annotations
import asyncio
import json
import os
from datetime import date, timedelta
from dotenv import load_dotenv
from browser_use import Agent, BrowserSession, ChatOpenAI
from browser_config import make_profile, RESUME_PATH
from parse_resume import parse_resume_to_profile
load_dotenv()
ASHBY_URL = os.environ.get("ASHBY_URL", "") # REQUIRED: the exact application page URL
APPLY_MODEL = os.environ.get("APPLY_MODEL", "gpt-4.1")
AUTO_SUBMIT = os.environ.get("AUTO_SUBMIT", "1") != "0"
MAX_STEPS = int(os.environ.get("MAX_STEPS", "55"))
# ---------------------------------------------------------------------------------------------
# FUTURE — LinkedIn discovery + login (kept for when we merge into one unit; intentionally OFF).
# Do NOT delete. When combining: prepend a login step using these, then navigate from the
# LinkedIn job to Apply, then fall through to the Ashby steps below.
#
# LINKEDIN_USER = os.environ.get("LINKEDIN_USER", "")
# LINKEDIN_PASS = os.environ.get("LINKEDIN_PASS", "")
# SENSITIVE_DATA = {
# "https://*.linkedin.com": {"linkedin_user": LINKEDIN_USER, "linkedin_pass": LINKEDIN_PASS}
# } if (LINKEDIN_USER and LINKEDIN_PASS) else None
# LOGIN_STEP = (
# "0. LOGIN: Go to https://www.linkedin.com/feed/. If a login form appears, log in with the "
# "placeholders linkedin_user / linkedin_pass. If a security checkpoint/CAPTCHA appears, STOP "
# "and report status='blocked' (a human must clear it). NOTE: prefer a pre-logged-in session "
# "to avoid repeated automated logins (each triggers a LinkedIn new-device alert)."
# )
# ---------------------------------------------------------------------------------------------
def build_task(profile: dict) -> str:
# Compute dates at runtime so this never goes stale. Suggest a realistic ~2-week-out start.
today = date.today()
start_target = today + timedelta(days=14)
today_str = today.strftime("%Y-%m-%d")
start_str = start_target.strftime("%Y-%m-%d (%B %-d, %Y)")
submit_clause = (
"Submit the application (click the final Submit / Apply button) — but only AFTER the "
"accuracy gate in step 6 passes."
if AUTO_SUBMIT else
"DO NOT click the final submit button. Stop at the review step and report it is ready for "
"a human to submit — but only AFTER the accuracy gate in step 6 passes."
)
return f"""You are completing ONE job application on a public OpenAI / AshbyHQ careers page on
behalf of the candidate below. Use ONLY the candidate's real data — never invent qualifications,
employers, dates, or work authorization.
CANDIDATE PROFILE (source of truth, parsed from their résumé):
{json.dumps(profile, indent=2, ensure_ascii=False)}
Résumé file to upload: {RESUME_PATH}
CONFIRMED FACTS (use these EXACT answers — they OVERRIDE any résumé guess):
- Preferred contact email: rahul.gupta2608@yahoo.com (the résumé email).
- The candidate lives in India, is an Indian citizen, and is authorized to work in India.
- This role is India-based, so: "authorized to work in the country where the job is located" = YES;
"require visa sponsorship" = NO. (If the page clearly shows a non-India location, answer
truthfully instead: not authorized / needs sponsorship for a US role. Honesty over optimism.)
- Availability: available to start soon. Today's date is {today_str}. For a "when can you start"
date field, pick a date ON OR AFTER today — NEVER a past date. A good choice is about two weeks
out: {start_str}. Use that unless the picker only allows a different valid future date.
- MOTIVATION (use this VERBATIM for any "what interests you about this job" / "why do you want to
work here" / "Additional Information" free-text field — a strong application never leaves this
blank):
"I want to help organizations actually put frontier AI to work, and the AI Deployment Engineer,
Codex role is the most direct place I've seen to do that. My background is hands-on ML and data
engineering — I shipped machine-learning systems at WalmartLabs and have built with LLM/GenAI
APIs, vector retrieval, and cloud infrastructure across AWS, Azure, and GCP. Codex is changing
how software gets written, and I'm excited to work directly with teams to turn that capability
into real, reliable deployments, then carry what I learn on the ground back into the product.
Being based in India, I'm especially motivated to help bring this technology to engineering
teams here. Deploying AI that people can trust and genuinely depend on is the work I most want
to be doing."
STEPS:
1. Go DIRECTLY to the application page: {ASHBY_URL}. No login is needed. The Ashby page is a
JavaScript app and may look BLANK for a few seconds — `wait` ~6s and re-read; refresh ONCE if
still blank. Only report a load failure after two waits AND a refresh.
2. LIVENESS CHECK: if the page shows "no longer accepting applications", a 404, or the role is
closed, STOP and report status="closed" with what you saw. Never fake a submit on a dead job.
3. AUTOFILL FIRST (most reliable way to fill fields): find the "Autofill from resume" control near
the top of the form and upload the résumé file THERE. `wait` ~8s for Ashby to parse it and
pre-fill fields. This populates name/email/experience automatically.
4. RÉSUMÉ ATTACHMENT (make-or-break — a half-finished upload got us flagged before):
- Ensure the required "Resume" field shows the file ATTACHED: the filename is visible and NO
spinner / "uploading" / "we're updating your application" text remains.
- If autofill did not attach it to the Resume field, upload the file there, `wait` ~8s, verify.
Retry up to 2 times. Do not continue until the attachment is CONFIRMED.
5. FILL EVERY FIELD THAT STRENGTHENS THE APPLICATION — not only the "*" required ones. "Required"
and "needed for a good application" are different: a blank motivation box is technically allowed
but makes a WEAK application. So fill every field you reasonably can from the profile + CONFIRMED
FACTS, and leave a field blank ONLY if it is truly irrelevant or asks for something the candidate
genuinely does not have. Specifics:
- "Where are you currently located?" = TYPEAHEAD: type "New Delhi", WAIT for the dropdown, and
CLICK the matching suggestion. Verify it no longer shows "Start typing…".
- "When can you start a new role?" = DATE PICKER: open it, CLICK an actual date, verify it no
longer shows "Pick date…".
- Work-authorization = YES, sponsorship = NO (India).
- "What interests you about this job" / "Additional Information" free-text box: fill it with the
MOTIVATION text from CONFIRMED FACTS verbatim. NEVER leave this blank — it is what makes or
breaks the application even though it has no "*".
- REQUIRED CONSENT CHECKBOX: tick "I confirm I have read the above." (the certification near the
bottom). This is REQUIRED even though it has no "*". After clicking it, VERIFY the box is
actually checked (a check mark / filled box) — re-click if it did not register.
6. ACCURACY GATE — GO/NO-GO (the whole point). Before doing step 7, go through the form field by
field and build a checklist of every field that must be filled. This includes: every "*" field;
every required consent CHECKBOX (e.g. "I confirm I have read the above") even with no "*"; AND
the motivation / "what interests you" / "Additional Information" free-text box. For each, record
its current value/state. A field counts as FILLED only if:
- text/select fields: a real value, NOT a placeholder ("Start typing…", "Pick date…",
"Type here…", "hello@example.com…");
- checkboxes/toggles: actually CHECKED/selected;
- the motivation box: contains the MOTIVATION text (not empty).
If ANY of these is empty, a placeholder, or an unchecked required box, it is NOT done: fill/tick
it now and re-check. You may NOT proceed to step 7 until EVERY one passes. Then CLOSE any open
date-picker/dropdown so the form is settled. Set all_required_filled=true only if every item
(including the consent checkbox and the motivation box) passes.
7. {submit_clause}
8. AFTER submit (if you submitted), VERIFY — never assume. Click Submit only ONCE, `wait` ~5s, read
the page. Do NOT rapid-click Submit (looks like spam).
- "still uploading / try again": upload wasn't done — re-verify step 4, then Submit once more.
- validation errors: fix the flagged fields, Submit once more.
- "flagged as possible spam": STOP, do NOT resubmit, report status="failed" with the exact text.
- Only report status="submitted" if you SEE an explicit confirmation ("Application submitted",
"Thank you for applying", or a reference id) — quote it verbatim. No proof => status="failed".
9. Final answer = JSON: {{"status": "submitted"|"ready_for_review"|"closed"|"failed"|"blocked",
"all_required_filled": true|false, "resume_upload_confirmed": true|false,
"required_fields": {{<field label>: <value you entered>, ...}},
"confirmation": <exact text/id or null>,
"uncertain": [<anything you guessed or skipped>], "notes": <short explanation>}}.
"""
async def main():
if not ASHBY_URL or "ashby" not in ASHBY_URL.lower():
print("✋ ASHBY_URL is not set (or doesn't look like an Ashby URL).")
print(" Open the OpenAI application tab and copy its address bar URL, then run:")
print(' ASHBY_URL="https://jobs.ashbyhq.com/openai/<id>/application" python apply_ashby.py')
return
print(f"[1/3] Parsing résumé via vision model ({RESUME_PATH})...")
profile = await parse_resume_to_profile(RESUME_PATH)
print(f" -> {profile.get('full_name')} · {profile.get('years_experience')} yrs · "
f"{len(profile.get('skills', []))} skills")
print(f"[2/3] Direct to Ashby (NO LinkedIn). model={APPLY_MODEL}, auto_submit={AUTO_SUBMIT}")
print(f" url: {ASHBY_URL}")
session = BrowserSession(browser_profile=make_profile())
agent = Agent(
task=build_task(profile),
llm=ChatOpenAI(model=APPLY_MODEL),
browser_session=session,
available_file_paths=[RESUME_PATH],
# sensitive_data not needed — no login on the Ashby page.
)
try:
history = await agent.run(max_steps=MAX_STEPS)
print("\n[3/3] ========== AGENT RESULT ==========")
print(history.final_result())
print("=====================================")
try:
shot = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ashby_result.png")
await session.take_screenshot(path=shot, full_page=True)
print(f"📸 Saved final-page screenshot -> {shot}")
except Exception as e:
print(f"(could not save screenshot: {e})")
finally:
try:
await asyncio.get_event_loop().run_in_executor(
None, input, "\nPress Enter to close the browser... "
)
except Exception:
pass
await session.kill()
if __name__ == "__main__":
asyncio.run(main())