launchway 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- launchway-0.1.0/Agents/__init__.py +1 -0
- launchway-0.1.0/Agents/agent_profile_service.py +207 -0
- launchway-0.1.0/Agents/bullet_generation/__init__.py +7 -0
- launchway-0.1.0/Agents/bullet_generation/project_bullet_generator.py +369 -0
- launchway-0.1.0/Agents/components/__init__.py +1 -0
- launchway-0.1.0/Agents/components/action_recorder.py +746 -0
- launchway-0.1.0/Agents/components/brains/__init__.py +1 -0
- launchway-0.1.0/Agents/components/brains/gemini_button_brain.py +260 -0
- launchway-0.1.0/Agents/components/brains/gemini_field_mapper.py +1568 -0
- launchway-0.1.0/Agents/components/brains/gemini_page_analyzer.py +174 -0
- launchway-0.1.0/Agents/components/custom_exceptions.py +5 -0
- launchway-0.1.0/Agents/components/detectors/__init__.py +1 -0
- launchway-0.1.0/Agents/components/detectors/account_creation_detector.py +308 -0
- launchway-0.1.0/Agents/components/detectors/application_form_detector.py +146 -0
- launchway-0.1.0/Agents/components/detectors/apply_detector.py +387 -0
- launchway-0.1.0/Agents/components/detectors/auth_page_detector.py +154 -0
- launchway-0.1.0/Agents/components/detectors/next_button_detector.py +101 -0
- launchway-0.1.0/Agents/components/detectors/popup_detector.py +106 -0
- launchway-0.1.0/Agents/components/detectors/required_field_detector.py +356 -0
- launchway-0.1.0/Agents/components/detectors/section_detector.py +115 -0
- launchway-0.1.0/Agents/components/detectors/sensitive_field_detector.py +195 -0
- launchway-0.1.0/Agents/components/detectors/submit_detector.py +100 -0
- launchway-0.1.0/Agents/components/exceptions/__init__.py +26 -0
- launchway-0.1.0/Agents/components/exceptions/field_exceptions.py +154 -0
- launchway-0.1.0/Agents/components/executors/__init__.py +1 -0
- launchway-0.1.0/Agents/components/executors/account_creation_handler.py +540 -0
- launchway-0.1.0/Agents/components/executors/ats_dropdown_handlers_v2.py +1117 -0
- launchway-0.1.0/Agents/components/executors/click_executor.py +107 -0
- launchway-0.1.0/Agents/components/executors/cmp_consent.py +187 -0
- launchway-0.1.0/Agents/components/executors/deterministic_field_mapper.py +742 -0
- launchway-0.1.0/Agents/components/executors/field_interactor_v2.py +2294 -0
- launchway-0.1.0/Agents/components/executors/generic_form_filler_v2_enhanced.py +1952 -0
- launchway-0.1.0/Agents/components/executors/iframe_helper.py +65 -0
- launchway-0.1.0/Agents/components/executors/intelligent_dropdown_selector.py +177 -0
- launchway-0.1.0/Agents/components/executors/learned_patterns_mapper.py +371 -0
- launchway-0.1.0/Agents/components/executors/popup_executor.py +73 -0
- launchway-0.1.0/Agents/components/executors/question_extractor.py +658 -0
- launchway-0.1.0/Agents/components/executors/section_filler.py +436 -0
- launchway-0.1.0/Agents/components/pattern_recorder.py +447 -0
- launchway-0.1.0/Agents/components/router/__init__.py +1 -0
- launchway-0.1.0/Agents/components/router/state_machine.py +292 -0
- launchway-0.1.0/Agents/components/services/__init__.py +6 -0
- launchway-0.1.0/Agents/components/services/company_credentials_service.py +263 -0
- launchway-0.1.0/Agents/components/session/__init__.py +1 -0
- launchway-0.1.0/Agents/components/session/session_manager.py +711 -0
- launchway-0.1.0/Agents/components/state/__init__.py +1 -0
- launchway-0.1.0/Agents/components/state/field_completion_tracker.py +176 -0
- launchway-0.1.0/Agents/components/utils/__init__.py +1 -0
- launchway-0.1.0/Agents/components/utils/google_docs_converter.py +149 -0
- launchway-0.1.0/Agents/components/validators/__init__.py +5 -0
- launchway-0.1.0/Agents/components/validators/field_value_validator.py +135 -0
- launchway-0.1.0/Agents/components/validators/nav_validator.py +102 -0
- launchway-0.1.0/Agents/cover_letter_tailoring.py +125 -0
- launchway-0.1.0/Agents/gemini_key_manager.py +226 -0
- launchway-0.1.0/Agents/gemini_query_optimizer.py +574 -0
- launchway-0.1.0/Agents/gemini_rate_limiter.py +237 -0
- launchway-0.1.0/Agents/hidden_browser_manager.py +205 -0
- launchway-0.1.0/Agents/improved_char_calc.py +208 -0
- launchway-0.1.0/Agents/job_api_adapters.py +1141 -0
- launchway-0.1.0/Agents/job_application_agent.py +3661 -0
- launchway-0.1.0/Agents/job_relevance_scorer.py +387 -0
- launchway-0.1.0/Agents/jobspy_adapter.py +208 -0
- launchway-0.1.0/Agents/latex_tailoring_agent.py +1220 -0
- launchway-0.1.0/Agents/mimikree_cache.py +101 -0
- launchway-0.1.0/Agents/mimikree_integration.py +401 -0
- launchway-0.1.0/Agents/multi_source_job_discovery_agent.py +774 -0
- launchway-0.1.0/Agents/persistent_browser_manager.py +434 -0
- launchway-0.1.0/Agents/project_selection/__init__.py +8 -0
- launchway-0.1.0/Agents/project_selection/mimikree_project_discovery.py +369 -0
- launchway-0.1.0/Agents/project_selection/relevance_engine.py +466 -0
- launchway-0.1.0/Agents/proxy_manager.py +290 -0
- launchway-0.1.0/Agents/resume_keyword_extractor.py +244 -0
- launchway-0.1.0/Agents/resume_tailoring_agent.py +2272 -0
- launchway-0.1.0/Agents/space_borrowing.py +209 -0
- launchway-0.1.0/Agents/systematic_tailoring_complete.py +1773 -0
- launchway-0.1.0/Agents/token.json +1 -0
- launchway-0.1.0/Agents/validation/__init__.py +8 -0
- launchway-0.1.0/Agents/validation/hallucination_detector.py +292 -0
- launchway-0.1.0/Agents/validation/semantic_validator.py +428 -0
- launchway-0.1.0/LICENSE +21 -0
- launchway-0.1.0/MANIFEST.in +36 -0
- launchway-0.1.0/PKG-INFO +148 -0
- launchway-0.1.0/README.md +80 -0
- launchway-0.1.0/launchway/__init__.py +7 -0
- launchway-0.1.0/launchway/__main__.py +6 -0
- launchway-0.1.0/launchway/api_client.py +270 -0
- launchway-0.1.0/launchway/cli/__init__.py +1 -0
- launchway-0.1.0/launchway/cli/agent.py +169 -0
- launchway-0.1.0/launchway/cli/mixins/__init__.py +1 -0
- launchway-0.1.0/launchway/cli/mixins/apply.py +350 -0
- launchway-0.1.0/launchway/cli/mixins/auth.py +362 -0
- launchway-0.1.0/launchway/cli/mixins/browser_setup.py +99 -0
- launchway-0.1.0/launchway/cli/mixins/continuous.py +912 -0
- launchway-0.1.0/launchway/cli/mixins/history.py +41 -0
- launchway-0.1.0/launchway/cli/mixins/job_search.py +201 -0
- launchway-0.1.0/launchway/cli/mixins/profile.py +934 -0
- launchway-0.1.0/launchway/cli/mixins/settings.py +115 -0
- launchway-0.1.0/launchway/cli/mixins/tailoring.py +230 -0
- launchway-0.1.0/launchway/cli/utils.py +49 -0
- launchway-0.1.0/launchway/config.py +157 -0
- launchway-0.1.0/launchway/postinstall.py +55 -0
- launchway-0.1.0/launchway/session.py +58 -0
- launchway-0.1.0/launchway.egg-info/PKG-INFO +148 -0
- launchway-0.1.0/launchway.egg-info/SOURCES.txt +108 -0
- launchway-0.1.0/launchway.egg-info/dependency_links.txt +1 -0
- launchway-0.1.0/launchway.egg-info/entry_points.txt +2 -0
- launchway-0.1.0/launchway.egg-info/requires.txt +46 -0
- launchway-0.1.0/launchway.egg-info/top_level.txt +2 -0
- launchway-0.1.0/pyproject.toml +147 -0
- launchway-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Launchway agent modules — browser automation, AI, resume tailoring."""
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Add parent directory to path to access database_config
|
|
5
|
+
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
6
|
+
|
|
7
|
+
from database_config import SessionLocal, User, UserProfile
|
|
8
|
+
from typing import Optional, Dict, Any, Union
|
|
9
|
+
import logging
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
class AgentProfileService:
|
|
13
|
+
"""Service for agents to access user profile data from PostgreSQL"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def get_profile_by_user_id(user_id: Union[str, UUID]) -> Optional[Dict[str, Any]]:
|
|
17
|
+
"""
|
|
18
|
+
Get complete profile data for a specific user
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
user_id: User UUID (can be string or UUID object)
|
|
22
|
+
"""
|
|
23
|
+
db = SessionLocal()
|
|
24
|
+
try:
|
|
25
|
+
# Convert string to UUID if needed
|
|
26
|
+
if isinstance(user_id, str):
|
|
27
|
+
user_id = UUID(user_id)
|
|
28
|
+
|
|
29
|
+
# Get user data
|
|
30
|
+
user = db.query(User).filter(User.id == user_id).first()
|
|
31
|
+
if not user:
|
|
32
|
+
logging.error(f"User with ID {user_id} not found")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Get profile data
|
|
36
|
+
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
|
37
|
+
|
|
38
|
+
# Build complete profile data in the format agents expect
|
|
39
|
+
profile_data = {
|
|
40
|
+
"resume_url": profile.resume_url if profile and profile.resume_url else "",
|
|
41
|
+
"resume_source_type": profile.resume_source_type if profile and profile.resume_source_type else "google_doc",
|
|
42
|
+
"latex_main_tex_path": profile.latex_main_tex_path if profile and profile.latex_main_tex_path else "",
|
|
43
|
+
"latex_file_manifest": profile.latex_file_manifest if profile and profile.latex_file_manifest else [],
|
|
44
|
+
"first name": user.first_name,
|
|
45
|
+
"last name": user.last_name,
|
|
46
|
+
"email": user.email,
|
|
47
|
+
"date of birth": profile.date_of_birth if profile and profile.date_of_birth else "",
|
|
48
|
+
"gender": profile.gender if profile and profile.gender else "",
|
|
49
|
+
"nationality": profile.nationality if profile and profile.nationality else "",
|
|
50
|
+
"preferred language": profile.preferred_language if profile and profile.preferred_language else "",
|
|
51
|
+
"phone": profile.phone if profile and profile.phone else "",
|
|
52
|
+
"address": profile.address if profile and profile.address else "",
|
|
53
|
+
"city": profile.city if profile and profile.city else "",
|
|
54
|
+
"state": profile.state if profile and profile.state else "",
|
|
55
|
+
"zip": profile.zip_code if profile and profile.zip_code else "",
|
|
56
|
+
"country": profile.country if profile and profile.country else "",
|
|
57
|
+
"country_code": profile.country_code if profile and profile.country_code else "",
|
|
58
|
+
"state_code": profile.state_code if profile and profile.state_code else "",
|
|
59
|
+
"linkedin": profile.linkedin if profile and profile.linkedin else "",
|
|
60
|
+
"github": profile.github if profile and profile.github else "",
|
|
61
|
+
"other links": profile.other_links if profile and profile.other_links else [""],
|
|
62
|
+
"education": profile.education if profile and profile.education else [
|
|
63
|
+
{
|
|
64
|
+
"degree": "",
|
|
65
|
+
"institution": "",
|
|
66
|
+
"graduation_year": "",
|
|
67
|
+
"gpa": "",
|
|
68
|
+
"relevant_courses": [""]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"work experience": profile.work_experience if profile and profile.work_experience else [
|
|
72
|
+
{
|
|
73
|
+
"title": "",
|
|
74
|
+
"company": "",
|
|
75
|
+
"start_date": "",
|
|
76
|
+
"end_date": "",
|
|
77
|
+
"description": "",
|
|
78
|
+
"achievements": [""]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"projects": profile.projects if profile and profile.projects else [
|
|
82
|
+
{
|
|
83
|
+
"name": "",
|
|
84
|
+
"description": "",
|
|
85
|
+
"technologies": [""],
|
|
86
|
+
"github_url": "",
|
|
87
|
+
"live_url": "",
|
|
88
|
+
"features": [""]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"skills": profile.skills if profile and profile.skills else {
|
|
92
|
+
"technical": [""],
|
|
93
|
+
"programming_languages": [""],
|
|
94
|
+
"frameworks": [""],
|
|
95
|
+
"tools": [""],
|
|
96
|
+
"soft_skills": [""],
|
|
97
|
+
"languages": [""]
|
|
98
|
+
},
|
|
99
|
+
"summary": profile.summary if profile and profile.summary else "",
|
|
100
|
+
"disabilities": profile.disabilities if profile and profile.disabilities else [],
|
|
101
|
+
"veteran status": profile.veteran_status if profile and profile.veteran_status else "",
|
|
102
|
+
"visa status": profile.visa_status if profile and profile.visa_status else "",
|
|
103
|
+
"visa sponsorship": profile.visa_sponsorship if profile and profile.visa_sponsorship else "",
|
|
104
|
+
"preferred location": profile.preferred_location if profile and profile.preferred_location else [""],
|
|
105
|
+
"willing to relocate": profile.willing_to_relocate if profile and profile.willing_to_relocate is not None else "",
|
|
106
|
+
# AI Engine — included so agents can build a GeminiKeyManager
|
|
107
|
+
"api_primary_mode": profile.api_primary_mode if profile and profile.api_primary_mode else "launchway",
|
|
108
|
+
"api_secondary_mode": profile.api_secondary_mode if profile and profile.api_secondary_mode else None,
|
|
109
|
+
# Decrypted key (server-side only; never sent to CLI clients)
|
|
110
|
+
"custom_gemini_api_key_decrypted": AgentProfileService._decrypt_api_key(
|
|
111
|
+
profile.custom_gemini_api_key if profile else None
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return profile_data
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logging.error(f"Error getting profile for user {user_id}: {e}")
|
|
119
|
+
return None
|
|
120
|
+
finally:
|
|
121
|
+
db.close()
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _decrypt_api_key(encrypted: Optional[str]) -> Optional[str]:
|
|
125
|
+
"""Decrypt the stored Gemini API key using the server's security manager."""
|
|
126
|
+
if not encrypted:
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
import sys, os
|
|
130
|
+
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'server'))
|
|
131
|
+
from security_manager import security_manager
|
|
132
|
+
return security_manager.decrypt_sensitive_data(encrypted)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logging.warning(f"Could not decrypt custom API key: {e}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def get_gemini_key_manager(user_id: Union[str, UUID]) -> Optional[Any]:
|
|
139
|
+
"""
|
|
140
|
+
Return a fully configured GeminiKeyManager for the given user.
|
|
141
|
+
Falls back to Launchway shared key if nothing is configured.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
profile = AgentProfileService.get_profile_by_user_id(user_id)
|
|
145
|
+
if not profile:
|
|
146
|
+
return None
|
|
147
|
+
from gemini_key_manager import GeminiKeyManager
|
|
148
|
+
return GeminiKeyManager.from_profile(profile)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logging.warning(f"Could not build GeminiKeyManager for {user_id}: {e}")
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def get_profile_by_email(email: str) -> Optional[Dict[str, Any]]:
|
|
155
|
+
"""Get complete profile data for a user by email"""
|
|
156
|
+
db = SessionLocal()
|
|
157
|
+
try:
|
|
158
|
+
user = db.query(User).filter(User.email == email).first()
|
|
159
|
+
if not user:
|
|
160
|
+
logging.error(f"User with email {email} not found")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
return AgentProfileService.get_profile_by_user_id(user.id)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logging.error(f"Error getting profile for email {email}: {e}")
|
|
167
|
+
return None
|
|
168
|
+
finally:
|
|
169
|
+
db.close()
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def get_latest_user_profile() -> Optional[Dict[str, Any]]:
|
|
173
|
+
"""Get the most recently created user's profile (for backward compatibility)"""
|
|
174
|
+
db = SessionLocal()
|
|
175
|
+
try:
|
|
176
|
+
# Get the most recently created user
|
|
177
|
+
latest_user = db.query(User).order_by(User.created_at.desc()).first()
|
|
178
|
+
if not latest_user:
|
|
179
|
+
logging.error("No users found in database")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return AgentProfileService.get_profile_by_user_id(latest_user.id)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logging.error(f"Error getting latest user profile: {e}")
|
|
186
|
+
return None
|
|
187
|
+
finally:
|
|
188
|
+
db.close()
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def validate_profile_completeness(profile_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
192
|
+
"""Validate if profile has sufficient data for job applications"""
|
|
193
|
+
required_fields = {
|
|
194
|
+
"first name": profile_data.get("first name", ""),
|
|
195
|
+
"last name": profile_data.get("last name", ""),
|
|
196
|
+
"email": profile_data.get("email", ""),
|
|
197
|
+
"phone": profile_data.get("phone", ""),
|
|
198
|
+
"resume_url": profile_data.get("resume_url", "")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
missing_fields = [field for field, value in required_fields.items() if not value]
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"is_complete": len(missing_fields) == 0,
|
|
205
|
+
"missing_fields": missing_fields,
|
|
206
|
+
"completion_percentage": ((len(required_fields) - len(missing_fields)) / len(required_fields)) * 100
|
|
207
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Bullet Generator
|
|
3
|
+
|
|
4
|
+
Generates tailored resume bullets for projects based on job requirements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
import google.generativeai as genai
|
|
11
|
+
|
|
12
|
+
# Set up logging
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProjectBulletGenerator:
|
|
17
|
+
"""Generates tailored project bullets for resumes"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, gemini_api_key: str, model_name: str = "gemini-2.5-flash"):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the bullet generator.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
gemini_api_key: Gemini API key
|
|
25
|
+
model_name: Gemini model to use
|
|
26
|
+
"""
|
|
27
|
+
genai.configure(api_key=gemini_api_key)
|
|
28
|
+
self.model = genai.GenerativeModel(model_name)
|
|
29
|
+
self.logger = logger
|
|
30
|
+
|
|
31
|
+
def generate_bullets(
|
|
32
|
+
self,
|
|
33
|
+
project: Dict,
|
|
34
|
+
job_keywords: List[str],
|
|
35
|
+
job_description: str,
|
|
36
|
+
target_bullet_count: int = 3,
|
|
37
|
+
char_limit_per_line: int = 90,
|
|
38
|
+
mimikree_context: Optional[str] = None
|
|
39
|
+
) -> List[str]:
|
|
40
|
+
"""
|
|
41
|
+
Generate tailored resume bullets for a project.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
project: Project dict with name, description, technologies, features
|
|
45
|
+
job_keywords: Keywords from job description to emphasize
|
|
46
|
+
job_description: Full job description for context
|
|
47
|
+
target_bullet_count: Number of bullets to generate
|
|
48
|
+
char_limit_per_line: Character limit per bullet line
|
|
49
|
+
mimikree_context: Optional additional context from Mimikree
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of bullet strings
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
# Build project context
|
|
56
|
+
project_context = f"""
|
|
57
|
+
PROJECT NAME: {project.get('name', 'Unknown Project')}
|
|
58
|
+
|
|
59
|
+
DESCRIPTION: {project.get('description', 'No description available')}
|
|
60
|
+
|
|
61
|
+
TECHNOLOGIES: {', '.join(project.get('technologies', []))}
|
|
62
|
+
|
|
63
|
+
FEATURES:
|
|
64
|
+
{chr(10).join([f"- {f}" for f in project.get('features', [])])}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if project.get('detailed_bullets'):
|
|
68
|
+
project_context += f"\n\nEXISTING BULLETS (for reference):\n"
|
|
69
|
+
project_context += "\n".join([f"- {b}" for b in project.get('detailed_bullets', [])])
|
|
70
|
+
|
|
71
|
+
# Prepare additional context section (avoid backslash in f-string)
|
|
72
|
+
additional_context = ""
|
|
73
|
+
if mimikree_context:
|
|
74
|
+
additional_context = f"ADDITIONAL CONTEXT:\n{mimikree_context}\n"
|
|
75
|
+
|
|
76
|
+
prompt = f"""Generate {target_bullet_count} tailored resume bullet points for this project.
|
|
77
|
+
|
|
78
|
+
{project_context}
|
|
79
|
+
|
|
80
|
+
JOB DESCRIPTION EXCERPT:
|
|
81
|
+
{job_description[:800]}
|
|
82
|
+
|
|
83
|
+
KEY JOB REQUIREMENTS TO EMPHASIZE:
|
|
84
|
+
{', '.join(job_keywords[:10])}
|
|
85
|
+
|
|
86
|
+
{additional_context}
|
|
87
|
+
|
|
88
|
+
REQUIREMENTS FOR BULLETS:
|
|
89
|
+
1. Each bullet MUST be 2-3 lines long (approximately {char_limit_per_line * 2}-{char_limit_per_line * 3} characters)
|
|
90
|
+
2. Start with strong action verbs (Built, Developed, Implemented, Designed, Created)
|
|
91
|
+
3. Include quantified metrics where possible (users, performance, scale, etc.)
|
|
92
|
+
4. Naturally incorporate relevant job keywords: {', '.join(job_keywords[:5])}
|
|
93
|
+
5. Highlight technical skills and impact
|
|
94
|
+
6. Be specific about technologies and methodologies used
|
|
95
|
+
7. Follow the format: [Action] [What] using [Technologies] to achieve [Impact/Result]
|
|
96
|
+
|
|
97
|
+
EXAMPLE FORMAT (adapt to this project):
|
|
98
|
+
Built scalable REST API using Node.js and Express, handling 10K+ requests/day and reducing response time by 40% through optimized database queries and caching strategies
|
|
99
|
+
|
|
100
|
+
Implemented real-time chat feature with WebSocket protocol and Redis pub/sub, enabling instant message delivery for 5K+ concurrent users with 99.9% uptime
|
|
101
|
+
|
|
102
|
+
CRITICAL OUTPUT RULES:
|
|
103
|
+
- Return EXACTLY {target_bullet_count} bullets
|
|
104
|
+
- Each bullet on a new line starting with a dash (-)
|
|
105
|
+
- NO numbering, NO extra formatting
|
|
106
|
+
- NO explanations or metadata
|
|
107
|
+
- Just the clean bullet text
|
|
108
|
+
|
|
109
|
+
Generate {target_bullet_count} bullets now:"""
|
|
110
|
+
|
|
111
|
+
response = self.model.generate_content(prompt)
|
|
112
|
+
bullets_text = response.text.strip()
|
|
113
|
+
|
|
114
|
+
# Parse bullets
|
|
115
|
+
bullets = []
|
|
116
|
+
for line in bullets_text.split('\n'):
|
|
117
|
+
line = line.strip()
|
|
118
|
+
# Remove leading dash, number, or asterisk
|
|
119
|
+
line = re.sub(r'^[-•*\d.)\]]+\s*', '', line)
|
|
120
|
+
|
|
121
|
+
if line and len(line) > 20: # Minimum meaningful length
|
|
122
|
+
bullets.append(line)
|
|
123
|
+
|
|
124
|
+
# Validate bullet count
|
|
125
|
+
if len(bullets) < target_bullet_count:
|
|
126
|
+
self.logger.warning(f"Generated only {len(bullets)} bullets, expected {target_bullet_count}")
|
|
127
|
+
elif len(bullets) > target_bullet_count:
|
|
128
|
+
bullets = bullets[:target_bullet_count]
|
|
129
|
+
|
|
130
|
+
# Validate bullet length (should be 2-3 lines)
|
|
131
|
+
validated_bullets = []
|
|
132
|
+
for bullet in bullets:
|
|
133
|
+
lines_count = len(bullet) // char_limit_per_line + (1 if len(bullet) % char_limit_per_line > 0 else 0)
|
|
134
|
+
|
|
135
|
+
if lines_count < 2:
|
|
136
|
+
self.logger.warning(f"Bullet too short ({lines_count} lines): {bullet[:50]}...")
|
|
137
|
+
elif lines_count > 4:
|
|
138
|
+
self.logger.warning(f"Bullet too long ({lines_count} lines): {bullet[:50]}...")
|
|
139
|
+
|
|
140
|
+
validated_bullets.append(bullet)
|
|
141
|
+
|
|
142
|
+
return validated_bullets
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.logger.error(f"Failed to generate bullets: {e}")
|
|
146
|
+
# Fallback: create basic bullets from project info
|
|
147
|
+
fallback_bullets = []
|
|
148
|
+
|
|
149
|
+
if project.get('description'):
|
|
150
|
+
fallback_bullets.append(project['description'])
|
|
151
|
+
|
|
152
|
+
for feature in project.get('features', [])[:2]:
|
|
153
|
+
if len(fallback_bullets) < target_bullet_count:
|
|
154
|
+
fallback_bullets.append(f"Implemented {feature} using {', '.join(project.get('technologies', [])[:2])}")
|
|
155
|
+
|
|
156
|
+
return fallback_bullets[:target_bullet_count]
|
|
157
|
+
|
|
158
|
+
def regenerate_bullet(
|
|
159
|
+
self,
|
|
160
|
+
original_bullet: str,
|
|
161
|
+
feedback: str,
|
|
162
|
+
job_keywords: List[str],
|
|
163
|
+
char_limit_per_line: int = 90
|
|
164
|
+
) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Regenerate a single bullet based on user feedback.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
original_bullet: The bullet to improve
|
|
170
|
+
feedback: User feedback (e.g., "add more metrics", "too generic")
|
|
171
|
+
job_keywords: Keywords to emphasize
|
|
172
|
+
char_limit_per_line: Character limit per line
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Improved bullet string
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
prompt = f"""Improve this resume bullet based on user feedback.
|
|
179
|
+
|
|
180
|
+
ORIGINAL BULLET:
|
|
181
|
+
{original_bullet}
|
|
182
|
+
|
|
183
|
+
USER FEEDBACK:
|
|
184
|
+
{feedback}
|
|
185
|
+
|
|
186
|
+
JOB KEYWORDS TO EMPHASIZE: {', '.join(job_keywords[:5])}
|
|
187
|
+
|
|
188
|
+
REQUIREMENTS:
|
|
189
|
+
- Keep it 2-3 lines long (~{char_limit_per_line * 2}-{char_limit_per_line * 3} characters)
|
|
190
|
+
- Address the user's feedback
|
|
191
|
+
- Maintain or add quantified metrics
|
|
192
|
+
- Incorporate relevant keywords naturally
|
|
193
|
+
- Keep strong action verb at start
|
|
194
|
+
|
|
195
|
+
Return ONLY the improved bullet text, no explanations."""
|
|
196
|
+
|
|
197
|
+
response = self.model.generate_content(prompt)
|
|
198
|
+
improved = response.text.strip()
|
|
199
|
+
|
|
200
|
+
# Clean up formatting
|
|
201
|
+
improved = re.sub(r'^[-•*\d.)\]]+\s*', '', improved)
|
|
202
|
+
|
|
203
|
+
return improved
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
self.logger.error(f"Failed to regenerate bullet: {e}")
|
|
207
|
+
return original_bullet
|
|
208
|
+
|
|
209
|
+
def generate_bullets_batch(
|
|
210
|
+
self,
|
|
211
|
+
projects: List[Dict],
|
|
212
|
+
job_keywords: List[str],
|
|
213
|
+
job_description: str,
|
|
214
|
+
bullets_per_project: int = 3
|
|
215
|
+
) -> Dict[str, List[str]]:
|
|
216
|
+
"""
|
|
217
|
+
Generate bullets for multiple projects in batch.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
projects: List of project dicts
|
|
221
|
+
job_keywords: Keywords from job description
|
|
222
|
+
job_description: Full job description
|
|
223
|
+
bullets_per_project: Number of bullets per project
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dict mapping project IDs/names to bullet lists
|
|
227
|
+
"""
|
|
228
|
+
results = {}
|
|
229
|
+
|
|
230
|
+
for project in projects:
|
|
231
|
+
project_id = project.get('id') or project.get('name', 'unknown')
|
|
232
|
+
|
|
233
|
+
bullets = self.generate_bullets(
|
|
234
|
+
project,
|
|
235
|
+
job_keywords,
|
|
236
|
+
job_description,
|
|
237
|
+
target_bullet_count=bullets_per_project
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
results[str(project_id)] = bullets
|
|
241
|
+
|
|
242
|
+
self.logger.info(f"Generated {len(bullets)} bullets for project: {project.get('name', 'Unknown')}")
|
|
243
|
+
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
def enhance_existing_bullets(
|
|
247
|
+
self,
|
|
248
|
+
project: Dict,
|
|
249
|
+
existing_bullets: List[str],
|
|
250
|
+
job_keywords: List[str],
|
|
251
|
+
job_description: str
|
|
252
|
+
) -> List[str]:
|
|
253
|
+
"""
|
|
254
|
+
Enhance existing project bullets to better match job requirements.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
project: Project dict
|
|
258
|
+
existing_bullets: Current bullets
|
|
259
|
+
job_keywords: Keywords from job description
|
|
260
|
+
job_description: Full job description
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Enhanced bullet list
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
bullets_text = "\n".join([f"- {b}" for b in existing_bullets])
|
|
267
|
+
|
|
268
|
+
prompt = f"""Enhance these project bullets to better match job requirements.
|
|
269
|
+
|
|
270
|
+
PROJECT: {project.get('name', 'Unknown')}
|
|
271
|
+
TECHNOLOGIES: {', '.join(project.get('technologies', []))}
|
|
272
|
+
|
|
273
|
+
CURRENT BULLETS:
|
|
274
|
+
{bullets_text}
|
|
275
|
+
|
|
276
|
+
JOB DESCRIPTION EXCERPT:
|
|
277
|
+
{job_description[:600]}
|
|
278
|
+
|
|
279
|
+
KEY JOB KEYWORDS: {', '.join(job_keywords[:10])}
|
|
280
|
+
|
|
281
|
+
TASK: Enhance each bullet to:
|
|
282
|
+
1. Better emphasize relevant keywords: {', '.join(job_keywords[:5])}
|
|
283
|
+
2. Add more quantified metrics where possible
|
|
284
|
+
3. Highlight technologies and methodologies relevant to the job
|
|
285
|
+
4. Maintain 2-3 lines per bullet
|
|
286
|
+
5. Keep all factual claims (don't fabricate metrics)
|
|
287
|
+
|
|
288
|
+
Return the enhanced bullets in the same order, one per line, starting with dash (-)."""
|
|
289
|
+
|
|
290
|
+
response = self.model.generate_content(prompt)
|
|
291
|
+
enhanced_text = response.text.strip()
|
|
292
|
+
|
|
293
|
+
# Parse enhanced bullets
|
|
294
|
+
enhanced = []
|
|
295
|
+
for line in enhanced_text.split('\n'):
|
|
296
|
+
line = line.strip()
|
|
297
|
+
line = re.sub(r'^[-•*\d.)\]]+\s*', '', line)
|
|
298
|
+
if line and len(line) > 20:
|
|
299
|
+
enhanced.append(line)
|
|
300
|
+
|
|
301
|
+
# Ensure same count as original
|
|
302
|
+
if len(enhanced) != len(existing_bullets):
|
|
303
|
+
self.logger.warning(f"Enhanced bullet count mismatch: {len(enhanced)} vs {len(existing_bullets)}")
|
|
304
|
+
return existing_bullets
|
|
305
|
+
|
|
306
|
+
return enhanced
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.error(f"Failed to enhance bullets: {e}")
|
|
310
|
+
return existing_bullets
|
|
311
|
+
|
|
312
|
+
def validate_bullet_quality(self, bullet: str) -> Dict[str, any]:
|
|
313
|
+
"""
|
|
314
|
+
Validate the quality of a generated bullet.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
bullet: Bullet text to validate
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dict with validation results:
|
|
321
|
+
- is_valid: Boolean
|
|
322
|
+
- issues: List of quality issues
|
|
323
|
+
- score: Quality score 0-100
|
|
324
|
+
"""
|
|
325
|
+
issues = []
|
|
326
|
+
score = 100.0
|
|
327
|
+
|
|
328
|
+
# Check 1: Length (should be 2-3 lines, ~160-270 chars)
|
|
329
|
+
if len(bullet) < 100:
|
|
330
|
+
issues.append("Too short - add more detail")
|
|
331
|
+
score -= 20
|
|
332
|
+
elif len(bullet) > 350:
|
|
333
|
+
issues.append("Too long - condense")
|
|
334
|
+
score -= 15
|
|
335
|
+
|
|
336
|
+
# Check 2: Starts with action verb
|
|
337
|
+
action_verbs = [
|
|
338
|
+
'built', 'developed', 'implemented', 'designed', 'created', 'engineered',
|
|
339
|
+
'architected', 'deployed', 'optimized', 'improved', 'led', 'managed'
|
|
340
|
+
]
|
|
341
|
+
if not any(bullet.lower().startswith(verb) for verb in action_verbs):
|
|
342
|
+
issues.append("Should start with strong action verb")
|
|
343
|
+
score -= 15
|
|
344
|
+
|
|
345
|
+
# Check 3: Contains technical terms
|
|
346
|
+
tech_pattern = r'\b[A-Z][a-z]+(?:\.[A-Z][a-z]+)*\b|[A-Z]{2,}'
|
|
347
|
+
if not re.search(tech_pattern, bullet):
|
|
348
|
+
issues.append("Should include specific technologies")
|
|
349
|
+
score -= 10
|
|
350
|
+
|
|
351
|
+
# Check 4: Contains metrics (numbers)
|
|
352
|
+
if not re.search(r'\d+[%+kKmM]?', bullet):
|
|
353
|
+
issues.append("Consider adding quantified metrics")
|
|
354
|
+
score -= 10
|
|
355
|
+
|
|
356
|
+
# Check 5: Not too generic
|
|
357
|
+
generic_phrases = ['various', 'multiple', 'several', 'different', 'many']
|
|
358
|
+
if any(phrase in bullet.lower() for phrase in generic_phrases):
|
|
359
|
+
issues.append("Avoid generic phrases - be specific")
|
|
360
|
+
score -= 10
|
|
361
|
+
|
|
362
|
+
is_valid = score >= 70
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
'is_valid': is_valid,
|
|
366
|
+
'issues': issues,
|
|
367
|
+
'score': score,
|
|
368
|
+
'length': len(bullet)
|
|
369
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent component sub-packages."""
|