diff --git a/python/job-application/main.py b/python/job-application/main.py new file mode 100644 index 0000000..b15829a --- /dev/null +++ b/python/job-application/main.py @@ -0,0 +1,198 @@ +import os +import asyncio +import time +import random +from typing import List +from dotenv import load_dotenv +from stagehand import Stagehand, StagehandConfig +from browserbase import Browserbase +from pydantic import BaseModel, Field, HttpUrl +import httpx + +# Load environment variables +load_dotenv() + + +# Define JobInfo schema with Pydantic +class JobInfo(BaseModel): + url: HttpUrl = Field(..., description="Job URL") + title: str = Field(..., description="Job title") + + +class JobsData(BaseModel): + jobs: List[JobInfo] + + +# Fetch project concurrency limit from Browserbase SDK (maxed at 5) +async def get_project_concurrency() -> int: + bb = Browserbase(api_key=os.environ.get("BROWSERBASE_API_KEY")) + + project = await asyncio.to_thread( + bb.projects.retrieve, + os.environ.get("BROWSERBASE_PROJECT_ID") + ) + return min(project.concurrency, 5) + + +# Generate random email +def generate_random_email() -> str: + random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8)) + return f"agent-{random_string}@example.com" + + +# Generate unique agent identifier +def generate_agent_id() -> str: + timestamp = int(time.time() * 1000) + random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=7)) + return f"agent-{timestamp}-{random_string}" + + +# Apply to a single job +async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): + async with semaphore: + config = StagehandConfig( + env="BROWSERBASE", + api_key=os.environ.get("BROWSERBASE_API_KEY"), + project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), + model_name="google/gemini-2.5-flash", + model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY") + ) + + try: + async with Stagehand(config) as stagehand: + print(f"[{job_info.title}] Session Started") + + session_id = None + if hasattr(stagehand, 'session_id'): + session_id = stagehand.session_id + elif hasattr(stagehand, 'browserbase_session_id'): + session_id = stagehand.browserbase_session_id + + if session_id: + print(f"[{job_info.title}] Watch live: https://browserbase.com/sessions/{session_id}") + + page = stagehand.page + + # Navigate to job URL + await page.goto(str(job_info.url)) + print(f"[{job_info.title}] Navigated to job page") + + # Click on the specific job + await page.act(f"click on {job_info.title}") + print(f"[{job_info.title}] Clicked on job") + + # Fill out the form + agent_id = generate_agent_id() + email = generate_random_email() + + print(f"[{job_info.title}] Agent ID: {agent_id}") + print(f"[{job_info.title}] Email: {email}") + + # Fill agent identifier + await page.act(f"type '{agent_id}' into the agent identifier field") + + # Fill contact endpoint + await page.act(f"type '{email}' into the contact endpoint field") + + # Fill deployment region + await page.act(f"type 'us-west-2' into the deployment region field") + + # Upload agent profile + upload_actions = await page.observe("find the file upload button for agent profile") + if upload_actions and len(upload_actions) > 0: + upload_action = upload_actions[0] + upload_selector = str(upload_action.selector) + if upload_selector: + file_input = page.locator(upload_selector) + + # Fetch resume from URL + resume_url = "https://agent-job-board.vercel.app/Agent%20Resume.pdf" + async with httpx.AsyncClient() as client: + response = await client.get(resume_url) + if response.status_code != 200: + raise Exception(f"Failed to fetch resume: {response.status_code}") + resume_buffer = response.content + + await file_input.set_input_files({ + "name": "Agent Resume.pdf", + "mimeType": "application/pdf", + "buffer": resume_buffer, + }) + print(f"[{job_info.title}] Uploaded resume from {resume_url}") + + # Select multi-region deployment + await page.act("select 'Yes' for multi region deployment") + + # Submit the form + await page.act("click deploy agent button") + + print(f"[{job_info.title}] Application submitted successfully!") + + except Exception as error: + print(f"[{job_info.title}] Error: {error}") + raise error + + +async def main(): + # Get project concurrency limit + max_concurrency = await get_project_concurrency() + print(f"Executing with concurrency limit: {max_concurrency}") + + config = StagehandConfig( + env="BROWSERBASE", + api_key=os.environ.get("BROWSERBASE_API_KEY"), + project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), + model_name="google/gemini-2.5-flash", + model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY") + ) + + async with Stagehand(config) as stagehand: + print("Main Stagehand Session Started") + + session_id = None + if hasattr(stagehand, 'session_id'): + session_id = stagehand.session_id + elif hasattr(stagehand, 'browserbase_session_id'): + session_id = stagehand.browserbase_session_id + + if session_id: + print(f"Watch live: https://browserbase.com/sessions/{session_id}") + + page = stagehand.page + + # Navigate to agent job board + await page.goto("https://agent-job-board.vercel.app/") + print("Navigated to agent-job-board.vercel.app") + + # Click on "View Jobs" button + await page.act("click on the view jobs button") + print("Clicked on view jobs button") + + # Extract all jobs with titles using extract + jobs_result = await page.extract( + "extract all job listings with their titles and URLs", + schema=JobsData + ) + + jobs_data = jobs_result.jobs + print(f"Found {len(jobs_data)} jobs") + + # Create semaphore with concurrency limit + semaphore = asyncio.Semaphore(max_concurrency) + + # Apply to all jobs in parallel with concurrency control + print(f"Starting to apply to {len(jobs_data)} jobs with max concurrency of {max_concurrency}") + + application_tasks = [apply_to_job(job, semaphore) for job in jobs_data] + + await asyncio.gather(*application_tasks) + + print("All applications completed!") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as err: + print(f"Error: {err}") + exit(1) diff --git a/typescript/job-application/index.ts b/typescript/job-application/index.ts new file mode 100644 index 0000000..995e3d5 --- /dev/null +++ b/typescript/job-application/index.ts @@ -0,0 +1,206 @@ +import "dotenv/config"; +import { Stagehand } from "@browserbasehq/stagehand"; +import Browserbase from "@browserbasehq/sdk"; +import { z } from "zod/v3"; + +// define JobInfo schema with zod: +const JobInfoSchema = z.object({ + url: z.string().url(), + title: z.string(), +}); + +type JobInfo = z.infer; + +// Fetch project concurrency limit from Browserbase SDK (maxed at 5) +export async function getProjectConcurrency(): Promise { + const bb = new Browserbase({ + apiKey: process.env.BROWSERBASE_API_KEY!, + }); + + const project = await bb.projects.retrieve(process.env.BROWSERBASE_PROJECT_ID!); + return Math.min(project.concurrency, 5); +} + +// Generate random email +export function generateRandomEmail(): string { + const randomString = Math.random().toString(36).substring(2, 10); + return `agent-${randomString}@example.com`; +} + +// Generate unique agent identifier +export function generateAgentId(): string { + return `agent-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} + +// Semaphore implementation for concurrency control +export function createSemaphore(maxConcurrency: number) { + let activeCount = 0; + const queue: (() => void)[] = []; + + const semaphore = () => + new Promise((resolve) => { + if (activeCount < maxConcurrency) { + activeCount++; + resolve(); + } else { + queue.push(resolve); + } + }); + + const release = () => { + activeCount--; + if (queue.length > 0) { + const next = queue.shift()!; + activeCount++; + next(); + } + }; + + return { semaphore, release }; +} + +// Apply to a single job +async function applyToJob(jobInfo: JobInfo, semaphore: () => Promise, release: () => void) { + await semaphore(); + + const stagehand = new Stagehand({ + env: "BROWSERBASE", + model: { + modelName: "google/gemini-2.5-flash", + apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, + } + }); + + try { + await stagehand.init(); + + console.log(`[${jobInfo.title}] Session Started`); + console.log( + `[${jobInfo.title}] Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}` + ); + + const page = stagehand.context.pages()[0]; + + // Navigate to job URL + await page.goto(jobInfo.url); + console.log(`[${jobInfo.title}] Navigated to job page`); + + // Click on the specific job + await stagehand.act(`click on ${jobInfo.title}`); + console.log(`[${jobInfo.title}] Clicked on job`); + + // Fill out the form + const agentId = generateAgentId(); + const email = generateRandomEmail(); + + console.log(`[${jobInfo.title}] Agent ID: ${agentId}`); + console.log(`[${jobInfo.title}] Email: ${email}`); + + // Fill agent identifier + await stagehand.act(`type '${agentId}' into the agent identifier field`); + + // Fill contact endpoint + await stagehand.act(`type '${email}' into the contact endpoint field`); + + // Fill deployment region + await stagehand.act(`type 'us-west-2' into the deployment region field`); + + // Upload agent profile + const [ uploadAction ] = await stagehand.observe("find the file upload button for agent profile"); + if (uploadAction) { + const uploadSelector = uploadAction.selector; + if (uploadSelector) { + const fileInput = page.locator(uploadSelector); + + // Fetch resume from URL + const resumeUrl = "https://agent-job-board.vercel.app/Agent%20Resume.pdf"; + const response = await fetch(resumeUrl); + if (!response.ok) { + throw new Error(`Failed to fetch resume: ${response.statusText}`); + } + const resumeBuffer = Buffer.from(await response.arrayBuffer()); + + await fileInput.setInputFiles({ + name: "Agent Resume.pdf", + mimeType: "application/pdf", + buffer: resumeBuffer, + }); + console.log(`[${jobInfo.title}] Uploaded resume from ${resumeUrl}`); + } + } + + // Select multi-region deployment + await stagehand.act(`select 'Yes' for multi region deployment`); + + // Submit the form + await stagehand.act(`click deploy agent button`); + + console.log(`[${jobInfo.title}] Application submitted successfully!`); + + await stagehand.close(); + } catch (error) { + console.error(`[${jobInfo.title}] Error:`, error); + await stagehand.close(); + throw error; + } finally { + release(); // Release the semaphore slot + } +} + +async function main() { + // Get project concurrency limit + const maxConcurrency = await getProjectConcurrency(); + console.log(`Executing with concurrency limit: ${maxConcurrency}`); + + const stagehand = new Stagehand({ + env: "BROWSERBASE", + model: { + modelName: "google/gemini-2.5-flash", + apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, + } + }); + + await stagehand.init(); + + console.log(`Main Stagehand Session Started`); + console.log( + `Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}` + ); + + const page = stagehand.context.pages()[0]; + + // Navigate to localhost + await page.goto("https://agent-job-board.vercel.app/"); + console.log("Navigated to agent-job-board.vercel.app"); + + // Click on "View Jobs" button + await stagehand.act("click on the view jobs button"); + console.log("Clicked on view jobs button"); + + // Extract all jobs with titles using extract + const jobsData = await stagehand.extract( + "extract all job listings with their titles and URLs", + z.array(JobInfoSchema) + ); + + console.log(`Found ${jobsData.length} jobs`); + + await stagehand.close(); + + // Create semaphore with concurrency limit + const { semaphore, release } = createSemaphore(maxConcurrency); + + // Apply to all jobs in parallel with concurrency control + console.log(`Starting to apply to ${jobsData.length} jobs with max concurrency of ${maxConcurrency}`); + + const applicationPromises = jobsData.map((job) => applyToJob(job, semaphore, release)); + + await Promise.all(applicationPromises); + + console.log("All applications completed!"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file