root
Job Application Simulator - fix reset endpoint
958590b
Raw
History Blame Contribute Delete
8.26 kB
"""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