Spaces:
Sleeping
Sleeping
| """Job Application Simulator - Client""" | |
| from typing import Optional, List, Union | |
| from openenv.core import EnvClient | |
| from models import ( | |
| JobAppState, | |
| JobAppObservation, | |
| SearchJobs, | |
| AnalyzeJob, | |
| WriteCoverLetter, | |
| SubmitApplication, | |
| NextJob, | |
| StepResult, | |
| ) | |
| class JobAppEnv(EnvClient): | |
| """ | |
| Client for the Job Application Simulator environment. | |
| Usage (async): | |
| async with JobAppEnv(base_url="...") as env: | |
| obs = await env.reset(profile_name="software_engineer") | |
| obs = await env.step(SearchJobs(keywords=["python", "remote"])) | |
| Usage (sync): | |
| with JobAppEnv(base_url="...").sync() as env: | |
| obs = env.reset() | |
| obs = env.step(AnalyzeJob(job_id="job_001")) | |
| """ | |
| def __init__(self, base_url: str = "http://localhost:8000"): | |
| super().__init__(base_url) | |
| self._episode_id: Optional[str] = None | |
| async def reset( | |
| self, | |
| profile_name: str = "default", | |
| budget: int = 10, | |
| difficulty: str = "normal" | |
| ) -> JobAppObservation: | |
| """ | |
| Reset the environment and start a new episode. | |
| Args: | |
| profile_name: Name of applicant profile to load | |
| budget: Maximum number of applications allowed | |
| difficulty: 'easy', 'normal', or 'hard' (affects job market) | |
| Returns: | |
| Initial observation with applicant profile | |
| """ | |
| result = await self._send_action({ | |
| "type": "reset", | |
| "profile_name": profile_name, | |
| "budget": budget, | |
| "difficulty": difficulty | |
| }) | |
| self._episode_id = result.get("episode_id") | |
| return self._parse_observation(result) | |
| async def step( | |
| self, | |
| action: Union[SearchJobs, AnalyzeJob, WriteCoverLetter, SubmitApplication, NextJob] | |
| ) -> StepResult: | |
| """ | |
| Execute an action in the environment. | |
| Args: | |
| action: One of SearchJobs, AnalyzeJob, WriteCoverLetter, SubmitApplication, NextJob | |
| Returns: | |
| StepResult with observation, reward, and done flag | |
| """ | |
| action_dict = self._action_to_dict(action) | |
| result = await self._send_action(action_dict) | |
| observation = self._parse_observation(result) | |
| return StepResult( | |
| observation=observation, | |
| reward=result.get("reward", 0.0), | |
| done=result.get("done", False), | |
| info=result.get("info", {}) | |
| ) | |
| async def state(self) -> JobAppState: | |
| """Get current episode state.""" | |
| result = await self._send_action({"type": "state"}) | |
| return self._parse_state(result) | |
| # ======================================================================== | |
| # CONVENIENCE METHODS | |
| # ======================================================================== | |
| async def search( | |
| self, | |
| keywords: List[str], | |
| location: Optional[str] = None, | |
| salary_min: Optional[int] = None, | |
| remote_only: bool = False | |
| ) -> JobAppObservation: | |
| """Search for jobs with given criteria.""" | |
| return await self.step(SearchJobs( | |
| keywords=keywords, | |
| location=location, | |
| salary_min=salary_min, | |
| remote_only=remote_only | |
| )) | |
| async def analyze(self, job_id: str) -> JobAppObservation: | |
| """Analyze a specific job for fit.""" | |
| return await self.step(AnalyzeJob(job_id=job_id)) | |
| async def write_letter( | |
| self, | |
| job_id: str, | |
| tone: str = "professional", | |
| highlight_skills: Optional[List[str]] = None | |
| ) -> JobAppObservation: | |
| """Write a cover letter for a job.""" | |
| return await self.step(WriteCoverLetter( | |
| job_id=job_id, | |
| tone=tone, | |
| highlight_skills=highlight_skills or [] | |
| )) | |
| async def apply( | |
| self, | |
| job_id: str, | |
| cover_letter: str, | |
| tailored_sections: Optional[dict] = None | |
| ) -> JobAppObservation: | |
| """Submit an application.""" | |
| return await self.step(SubmitApplication( | |
| job_id=job_id, | |
| cover_letter=cover_letter, | |
| tailored_resume_sections=tailored_sections or {} | |
| )) | |
| async def next_job(self) -> JobAppObservation: | |
| """Move to next job in results.""" | |
| return await self.step(NextJob()) | |
| # ======================================================================== | |
| # INTERNAL HELPERS | |
| # ======================================================================== | |
| def _action_to_dict(self, action) -> dict: | |
| """Convert action dataclass to dictionary.""" | |
| if isinstance(action, SearchJobs): | |
| return { | |
| "type": "search_jobs", | |
| "keywords": action.keywords, | |
| "location": action.location, | |
| "salary_min": action.salary_min, | |
| "job_type": action.job_type, | |
| "remote_only": action.remote_only | |
| } | |
| elif isinstance(action, AnalyzeJob): | |
| return { | |
| "type": "analyze_job", | |
| "job_id": action.job_id | |
| } | |
| elif isinstance(action, WriteCoverLetter): | |
| return { | |
| "type": "write_cover_letter", | |
| "job_id": action.job_id, | |
| "tone": action.tone, | |
| "highlight_skills": action.highlight_skills | |
| } | |
| elif isinstance(action, SubmitApplication): | |
| return { | |
| "type": "submit_application", | |
| "job_id": action.job_id, | |
| "cover_letter": action.cover_letter, | |
| "tailored_resume_sections": action.tailored_resume_sections | |
| } | |
| elif isinstance(action, NextJob): | |
| return {"type": "next_job"} | |
| else: | |
| raise ValueError(f"Unknown action type: {type(action)}") | |
| def _parse_observation(self, data: dict) -> JobAppObservation: | |
| """Parse server response into observation.""" | |
| from models import JobPosting, JobAnalysis | |
| job_listings = [ | |
| JobPosting(**job) for job in data.get("job_listings", []) | |
| ] | |
| current_job = None | |
| if data.get("current_job"): | |
| current_job = JobPosting(**data["current_job"]) | |
| analysis_result = None | |
| if data.get("analysis_result"): | |
| analysis_result = JobAnalysis(**data["analysis_result"]) | |
| return JobAppObservation( | |
| job_listings=job_listings, | |
| current_job=current_job, | |
| analysis_result=analysis_result, | |
| cover_letter=data.get("cover_letter"), | |
| cover_letter_quality=data.get("cover_letter_quality"), | |
| application_status=data.get("application_status"), | |
| budget_remaining=data.get("budget_remaining", 10), | |
| applications_count=data.get("applications_count", 0), | |
| message=data.get("message", ""), | |
| done=data.get("done", False), | |
| reward=data.get("reward", 0.0) | |
| ) | |
| def _parse_state(self, data: dict) -> JobAppState: | |
| """Parse server response into state.""" | |
| from models import JobPosting, ApplicantProfile, SubmittedApplication | |
| state = JobAppState( | |
| episode_id=data.get("episode_id", ""), | |
| step_count=data.get("step_count", 0), | |
| budget_remaining=data.get("budget_remaining", 10), | |
| total_reward=data.get("total_reward", 0.0) | |
| ) | |
| if data.get("current_job"): | |
| state.current_job = JobPosting(**data["current_job"]) | |
| if data.get("applicant_profile"): | |
| state.applicant_profile = ApplicantProfile(**data["applicant_profile"]) | |
| state.applications_submitted = [ | |
| SubmittedApplication(**app) | |
| for app in data.get("applications_submitted", []) | |
| ] | |
| state.last_search_results = [ | |
| JobPosting(**job) | |
| for job in data.get("last_search_results", []) | |
| ] | |
| return state | |